diff --git a/.buildpath b/.buildpath
new file mode 100644
index 0000000..3675c09
--- /dev/null
+++ b/.buildpath
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..f81e7dc
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c442d7c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/target/
+*.log
+*~
+*.md.html
+/.dbeaver
diff --git a/.project b/.project
new file mode 100644
index 0000000..da9bb9e
--- /dev/null
+++ b/.project
@@ -0,0 +1,30 @@
+
+
+ row-level-security-lua
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.dltk.core.scriptbuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+ org.eclipse.ldt.nature
+ org.jkiss.dbeaver.DBeaverNature
+
+
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..84d20e0
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/lua=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/lua=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/=UTF-8
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..85a3c97
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,392 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=11
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
+org.eclipse.jdt.core.compiler.release=disabled
+org.eclipse.jdt.core.compiler.source=11
+org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false
+org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false
+org.eclipse.jdt.core.formatter.align_with_spaces=false
+org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=0
+org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0
+org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_module_statements=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16
+org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_record_components=16
+org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0
+org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0
+org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=true
+org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=false
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=false
+org.eclipse.jdt.core.formatter.comment.indent_tag_description=false
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=120
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert
+org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
+org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never
+org.eclipse.jdt.core.formatter.lineSplit=120
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
+org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines
+org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=4
+org.eclipse.jdt.core.formatter.text_block_indentation=0
+org.eclipse.jdt.core.formatter.use_on_off_tags=false
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=true
+org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false
+org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
+org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..edf22a7
--- /dev/null
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,146 @@
+cleanup.add_default_serial_version_id=true
+cleanup.add_generated_serial_version_id=false
+cleanup.add_missing_annotations=true
+cleanup.add_missing_deprecated_annotations=true
+cleanup.add_missing_methods=false
+cleanup.add_missing_nls_tags=false
+cleanup.add_missing_override_annotations=true
+cleanup.add_missing_override_annotations_interface_methods=true
+cleanup.add_serial_version_id=false
+cleanup.always_use_blocks=true
+cleanup.always_use_parentheses_in_expressions=true
+cleanup.always_use_this_for_non_static_field_access=true
+cleanup.always_use_this_for_non_static_method_access=false
+cleanup.convert_functional_interfaces=true
+cleanup.convert_to_enhanced_for_loop=true
+cleanup.convert_to_enhanced_for_loop_if_loop_var_used=false
+cleanup.correct_indentation=true
+cleanup.format_source_code=true
+cleanup.format_source_code_changes_only=false
+cleanup.insert_inferred_type_arguments=false
+cleanup.lazy_logical_operator=false
+cleanup.make_local_variable_final=true
+cleanup.make_parameters_final=true
+cleanup.make_private_fields_final=true
+cleanup.make_type_abstract_if_missing_method=false
+cleanup.make_variable_declarations_final=true
+cleanup.merge_conditional_blocks=false
+cleanup.never_use_blocks=false
+cleanup.never_use_parentheses_in_expressions=false
+cleanup.number_suffix=true
+cleanup.organize_imports=true
+cleanup.push_down_negation=false
+cleanup.qualify_static_field_accesses_with_declaring_class=false
+cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+cleanup.qualify_static_member_accesses_with_declaring_class=true
+cleanup.qualify_static_method_accesses_with_declaring_class=false
+cleanup.remove_private_constructors=true
+cleanup.remove_redundant_modifiers=false
+cleanup.remove_redundant_semicolons=false
+cleanup.remove_redundant_type_arguments=false
+cleanup.remove_trailing_whitespaces=true
+cleanup.remove_trailing_whitespaces_all=true
+cleanup.remove_trailing_whitespaces_ignore_empty=false
+cleanup.remove_unnecessary_array_creation=false
+cleanup.remove_unnecessary_casts=true
+cleanup.remove_unnecessary_nls_tags=true
+cleanup.remove_unused_imports=true
+cleanup.remove_unused_local_variables=false
+cleanup.remove_unused_private_fields=true
+cleanup.remove_unused_private_members=false
+cleanup.remove_unused_private_methods=true
+cleanup.remove_unused_private_types=true
+cleanup.simplify_lambda_expression_and_method_ref=true
+cleanup.sort_members=false
+cleanup.sort_members_all=false
+cleanup.use_anonymous_class_creation=false
+cleanup.use_autoboxing=false
+cleanup.use_blocks=true
+cleanup.use_blocks_only_for_return_and_throw=false
+cleanup.use_directly_map_method=false
+cleanup.use_lambda=true
+cleanup.use_parentheses_in_expressions=true
+cleanup.use_this_for_non_static_field_access=true
+cleanup.use_this_for_non_static_field_access_only_if_necessary=false
+cleanup.use_this_for_non_static_method_access=false
+cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+cleanup.use_unboxing=false
+cleanup.use_var=false
+cleanup_profile=_Exasol
+cleanup_settings_version=2
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Exasol
+formatter_settings_version=19
+org.eclipse.jdt.ui.text.custom_code_templates=
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=true
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=true
+sp_cleanup.always_use_this_for_non_static_field_access=true
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_functional_interfaces=true
+sp_cleanup.convert_to_enhanced_for_loop=true
+sp_cleanup.convert_to_enhanced_for_loop_if_loop_var_used=false
+sp_cleanup.correct_indentation=true
+sp_cleanup.format_source_code=true
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.insert_inferred_type_arguments=false
+sp_cleanup.lazy_logical_operator=true
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=true
+sp_cleanup.merge_conditional_blocks=true
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=false
+sp_cleanup.number_suffix=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=true
+sp_cleanup.push_down_negation=true
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=true
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_redundant_modifiers=true
+sp_cleanup.remove_redundant_semicolons=true
+sp_cleanup.remove_redundant_type_arguments=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_array_creation=true
+sp_cleanup.remove_unnecessary_casts=true
+sp_cleanup.remove_unnecessary_nls_tags=true
+sp_cleanup.remove_unused_imports=true
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.simplify_lambda_expression_and_method_ref=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_anonymous_class_creation=false
+sp_cleanup.use_autoboxing=true
+sp_cleanup.use_blocks=true
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_directly_map_method=true
+sp_cleanup.use_lambda=true
+sp_cleanup.use_parentheses_in_expressions=true
+sp_cleanup.use_this_for_non_static_field_access=true
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+sp_cleanup.use_unboxing=true
+sp_cleanup.use_var=false
diff --git a/.settings/org.eclipse.ldt.prefs b/.settings/org.eclipse.ldt.prefs
new file mode 100644
index 0000000..3b1203d
--- /dev/null
+++ b/.settings/org.eclipse.ldt.prefs
@@ -0,0 +1,2 @@
+Grammar__default_id=lua-5.1
+eclipse.preferences.version=1
diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/.settings/org.moreunit.core.prefs b/.settings/org.moreunit.core.prefs
new file mode 100644
index 0000000..cc3272c
--- /dev/null
+++ b/.settings/org.moreunit.core.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+org.moreunit.core.anyLanguage.active=true
+org.moreunit.core.anyLanguage.fileWordSeparator=_
+org.moreunit.core.anyLanguage.srcFolderPathTemplate=${srcProject}/src/main/lua/(**)
+org.moreunit.core.anyLanguage.testFileNameTemplate=test_${srcFile}
+org.moreunit.core.anyLanguage.testFolderPathTemplate=${srcProject}/src/test/lua/\\1
diff --git a/Findings.md b/Findings.md
new file mode 100644
index 0000000..8825187
--- /dev/null
+++ b/Findings.md
@@ -0,0 +1,3 @@
+# Prototype findings
+
+* `CREATE OR REPLACE LUA ADAPTER SCRIPT` does not work. Code seems cached. Requires dropping the script and recreating it.
diff --git a/README b/README
deleted file mode 100644
index b40eced..0000000
--- a/README
+++ /dev/null
@@ -1 +0,0 @@
-# row-level-security-lua
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..17cb13a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,135 @@
+# Row Level Security (Lua)
+
+
+
+[![Build Status](https://api.travis-ci.com/exasol/row-level-security.svg?branch=master)](https://travis-ci.org/exasol/row-level-security-lua)
+
+SonarCloud results:
+
+[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=alert_status)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+
+[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=security_rating)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=sqale_index)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+
+[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=code_smells)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=coverage)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=duplicated_lines_density)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=com.exasol%3Arow-level-security&metric=ncloc)](https://sonarcloud.io/dashboard?id=com.exasol%3Arow-level-security-lua)
+
+Protect access to database tables on a per-row level based on roles and / or tenants.
+
+## Features
+
+Restrict access to rows (datasets) in tables to …
+
+* set of roles
+* tenants (owners)
+* group of users
+* combination of group and tenant
+* combination of group and role
+
+## Table of Contents
+
+### Information for Users
+
+* [User Guide](doc/user_guide/user_guide.md)
+* [Tutorial](doc/user_guide/tutorial.md)
+* [Changelog](doc/changes/changelog.md)
+
+### Information for Contributors
+
+Requirement, design documents and coverage tags are written in [OpenFastTrace](https://github.com/itsallcode/openfasttrace) format.
+
+* [System Requirement Specification](doc/system_requirements.md)
+* [Design](doc/design.md)
+* [Developer Guide](doc/developer_guide.md)
+
+### Run Time Dependencies
+
+Running the RLS Lua Virtual Schema requires a Exasol with built-in Lua 5.1 or later.
+
+| Dependency | Purpose | License |
+|------------------------------------------|--------------------------------------------------------|-------------------------------|
+| [Lua CJSON][luacjson] | JSON parsing and writing | MIT License |
+| [LuaSocket][luasocket] | Socket communication | MIT License |
+
+Note that Lua CSON and LuaSucket both are pre-installed on an Exasol database. For local unit testing you need to install them on the test machine though.
+
+[luacjson]: https://www.kyne.com.au/~mark/software/lua-cjson.php
+[luasocket]: http://w3.impa.br/~diego/software/luasocket/
+
+### Test Dependencies
+
+#### Unit Test Dependencies
+
+Unit tests are written in Lua.
+
+| Dependency | Purpose | License |
+|------------------------------------------|--------------------------------------------------------|-------------------------------|
+| [luaunit][luaunit] | Unit testing framework | BSD License |
+| [Mockagne][mockagne] | Mocking framework | MIT License |
+
+[luaunit]: https://github.com/bluebird75/luaunit
+[mockagne]: https://github.com/vertti/mockagne
+
+#### Integration Test Dependencies
+
+The integration tests require `exasol-testcontainers` to provide an Exasol instance. The are written in Java and require version 11 or later.
+
+| Dependency | Purpose | License |
+|----------------------------------------------------|--------------------------------------------------------|-------------------------------|
+| [Exasol Testcontainers][exasol-testcontainers] | Integration test Exasol instance on Docker | MIT License |
+| [Hamcrest Resultset Matcher][hamcrest-rs-matcher] | Validating JDBC resultsets | MIT License |
+| [Java Hamcrest][java-hamcrest] | Checking for conditions in code via matchers | BSD License |
+| [JUnit][junit5] | Unit testing framework | Eclipse Public License 1.0 |
+| [Mockito][mockito] | Mocking framework | MIT License |
+| [Test Database Builder][tddb-java] | Framework for writing database integration tests | MIT License |
+| [Testcontainers][testcontainers] | Container-based integration tests | MIT License |
+| [SLF4J][slf4j] | Logging facade | MIT License |
+
+[exasol-testcontainers]: https://github.com/exasol/exasol-testcontainers
+[hamcrest-rs-matcher]: https://github.com/exasol/hamcrest-resultset-matcher
+[java-hamcrest]: http://hamcrest.org/JavaHamcrest/
+[junit5]: https://junit.org/junit5
+[mockito]: http://site.mockito.org/
+[tddb-java]: https://github.com/exasol/test-db-builder-java
+[testcontainers]: https://www.testcontainers.org/
+[slf4j]: http://www.slf4j.org/
+
+### Build Dependencies
+
+This project has a complex build setup due to the mixture of Lua and Java. [Apache Maven][maven] serves as the main build tool.
+
+Lua build steps are also encapsulated by Maven.
+
+| Dependency | Purpose | License |
+|-------------------------------------------|--------------------------------------------------------|-------------------------------|
+| [Amalg][amalg] | Bundling Lua modules (and scripts) | MIT License |
+| [Apache Maven][maven] | Build tool | Apache License 2.0 |
+| [Build Helper Maven Plugin][build-helper] | Register non-standard source directories (here Lua) | MIT License |
+| [Exec Maven Plugin][exec] | Execute external processes | Apache License 2.0 |
+| [LuaRocks][luarocks] | Package management | MIT License |
+| [Maven Assembly Plugin][assembly] | Building JAR archives | Apache License 2.0 |
+| [Maven Compiler Plugin][compiler] | Setting required Java version | Apache License 2.0 |
+| [Maven Failsafe Plugin][failsafe] | Integration testing | Apache License 2.0 |
+| [Maven Jacoco Plugin][jacoco] | Code coverage metering | Eclipse Public License 2.0 |
+| [Maven Source Plugin][source] | Creating a source code JAR | Apache License 2.0 |
+| [Maven Surefire Plugin][surefire] | Unit testing | Apache License 2.0 |
+| [OpenFastTrace Maven Plugin][oft] |Requirement Tracing | GPL V3 |
+| [OSS Index Maven Plugin][oss-index] | Dependency security monitoring | Apache License 2.0 |
+
+[amalg]: https://github.com/siffiejoe/lua-amalg
+[assembly]: https://maven.apache.org/plugins/maven-assembly-plugin/
+[build-helper]: http://www.mojohaus.org/build-helper-maven-plugin/
+[compiler]: https://maven.apache.org/plugins/maven-compiler-plugin/
+[exec]: https://www.mojohaus.org/exec-maven-plugin/
+[failsafe]: https://maven.apache.org/surefire/maven-surefire-plugin/
+[jacoco]: https://www.eclemma.org/jacoco/trunk/doc/maven.html
+[luarocks]: https://luarocks.org/
+[maven]: https://maven.apache.org/
+[oft]: https://github.com/itsallcode/openfasttrace-maven-plugin
+[oss-index]: https://sonatype.github.io/ossindex-maven/maven-plugin/
+[source]: https://maven.apache.org/plugins/maven-source-plugin/
+[surefire]: https://maven.apache.org/surefire/maven-surefire-plugin/
\ No newline at end of file
diff --git a/doc/developer_guide/developer_guide.md b/doc/developer_guide/developer_guide.md
new file mode 100644
index 0000000..f9f1e88
--- /dev/null
+++ b/doc/developer_guide/developer_guide.md
@@ -0,0 +1,47 @@
+# Developer Guide
+
+## Preparation
+
+Before you can build and test the application, you need to install Lua packages on the build machine.
+
+### Installing LuaRocks
+
+First install the package manager LuaRocks.
+
+```bash
+apt install luarocks
+```
+
+Now update your `LUA_PATH`, so that it contains the packages (aka. "rocks"). You can auto-generate that path.
+
+```bash
+luarocks path
+```
+
+Of course generating it is not enough, you also need to make sure the export is actually executed — preferably automatically each time. Here is an example that appends the path to the `.bashrc`:
+
+```bash
+luarocks path >> ~/.bashrc
+```
+
+### Installing the Required Lua Packages
+
+You need the packages for unit testing, mocking and JSON processing.
+
+Execute as `root` or modify to install in your home directory:
+
+```bash
+sudo luarocks install LuaUnit
+sudo luarocks install Mockagne
+sudo luarocks install lua-cjson
+```
+
+Most of those packages are only required for testing. While `cjson` is needed at runtime, it is prepackaged with Exasol, so no need to install it at runtime.
+
+### Bundling the Main Script and the Modules
+
+As most non-trivial pieces of software, `row-level-security-lua` is modularized. While it is possible to install individual modules as Lua scripts in Exasol, this is also a lot of work. And the more modules you install individually, the higher the chances you forget to update one of them. A safer and more convenient way is to bundle everything into one script before the installation using [lua-amalg](https://github.com/siffiejoe/lua-amalg/).
+
+To make this process easier, the [Maven POM file](../../pom.xml) contains an execution that automates this step. Still it is necessary to add new modules by hand in the list of modules to be bundled in the POM.
+
+Note that the entry point `request_dispatcher.lua` is a regular Lua script that must be added to the bundle using the `-s` switch and its relative path. The remaining bundle elements are Lua modules and must be listed in dot-notation.
diff --git a/doc/user_guide/user_guide.md b/doc/user_guide/user_guide.md
new file mode 100644
index 0000000..408fa69
--- /dev/null
+++ b/doc/user_guide/user_guide.md
@@ -0,0 +1,3 @@
+## Known Limitations
+
+* `SELECT *` is not yet supported due to an issue between the core database and the LUA Virtual Schemas in push-down requests (SPOT-10626)
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f235f54
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,266 @@
+
+ 4.0.0
+ com.exasol
+ row-level-security-lua
+ 0.1.0
+ Exasol Row Level Security (Lua)
+ This projects provides a plug-in to the Exasol database that adds per-row access control.
+
+ UTF-8
+ UTF-8
+ 11
+ 5.6.2
+ 1.6.2
+ 3.0.0-M4
+ target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml
+
+ ${basedir}/src/main/lua
+ ${basedir}/src/test/lua
+ /usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;./?.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua;/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua
+ ${lua.src}/administration
+
+
+
+ maven.exasol.com
+ https://maven.exasol.com/artifactory/exasol-releases
+
+ false
+
+
+
+
+
+ com.exasol
+ exasol-jdbc
+ 6.2.5
+ test
+
+
+ com.exasol
+ exasol-testcontainers
+ 3.0.0
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ 1.13.0
+ test
+
+
+ org.hamcrest
+ hamcrest
+ 2.2
+ test
+
+
+ com.exasol
+ hamcrest-resultset-matcher
+ 1.1.0
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+ org.junit.platform
+ junit-platform-runner
+ ${junit.platform.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+
+
+ org.slf4j
+ slf4j-jdk14
+ 1.7.30
+ test
+
+
+ com.exasol
+ test-db-builder-java
+ 0.2.0
+ test
+
+
+
+
+
+
+ maven-surefire-plugin
+ ${surefire.and.failsafe.plugin.version}
+
+ true
+
+
+
+ default-test
+ none
+
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ ${java.version}
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${surefire.and.failsafe.plugin.version}
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.0.0
+
+
+ Lua Unit Tests
+ test
+
+ exec
+
+
+ ${project.basedir}/tools/unittest.sh
+ true
+
+ ${project.basedir}
+
+
+
+
+
+ Lua Virtual Schema Bundle
+ package
+
+ exec
+
+
+ lua
+ true
+ ${lua.src}
+
+ ${basedir}/tools/amalg.lua
+ -o
+ ${basedir}/target/row-level-security-dist-${project.version}.lua
+
+ -s
+ exasolvs/request_dispatcher.lua
+
+ exasolvs.query_renderer
+ exasolrls.table_protection_status
+ exasolrls.adapter
+ exasolrls.metadata_reader
+ exasolrls.query_rewriter
+ exasollog.log
+
+
+
+
+
+
+
+ org.sonatype.ossindex.maven
+ ossindex-maven-plugin
+ 3.1.0
+
+
+ audit-dependencies
+ package
+
+ audit
+
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.2.0
+
+
+ generate-sources
+
+ add-source
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ [1.0,)
+
+ exec
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/lua/exasollog/log.lua b/src/main/lua/exasollog/log.lua
new file mode 100644
index 0000000..c604059
--- /dev/null
+++ b/src/main/lua/exasollog/log.lua
@@ -0,0 +1,221 @@
+local levels = {NONE = 1, FATAL = 2, ERROR = 3, WARN = 4, INFO = 5, CONFIG = 6, DEBUG = 7, TRACE = 8}
+
+---
+-- This module implements a remote log client with the ability to fall back to console logging in case no connection
+-- to a remote log receiver is established.
+--
+-- You can optionally use a high resolution timer for performance monitoring. Since Lua's os.date()
+-- function only has a resolution of seconds, that timer uses socket.gettime(). Note that the values
+-- you are getting are not the milliseconds of a second, but the milliseconds counted from when the module was first
+-- loaded — which is typically at the very beginning of the software using this module.
+--
+-- Use the init() method to set some global parameters for this module.
+--
+-- This method allows you to set parameters that apply to all subsequent calls to logging methods. While it is possible
+-- to change these settings at runtime, the recommended way is to do this once only, before you use the log for the
+-- first time.
+--
+--
+-- You can use a high resolution timer. Note that these are
not
the sub-second units of the timestamp! Lua
+-- timestamps only offer second resolution. Rather you get a time difference in milliseconds counted from the first time
+-- the log is opened.
+--
+-- @param timestamp_pattern layout of timestamps displayed in the logs
+--
+-- @param use_high_resolution_time switch high resolution time display on or off
+--
+-- @return module loader
+--
+function M.init(timestamp_pattern, use_high_resolution_time)
+ M.timestamp_pattern = timestamp_pattern
+ if use_high_resolution_time ~= nil then
+ M.use_high_resolution_time = use_high_resolution_time
+ end
+ return M
+end
+
+---
+-- Set the log client name.
+--
+-- This is the name presented when the log is first opened. We recommend using the name of the application or script
+-- that uses the log and a version number.
+--
+--
+function M.set_client_name(log_client_name)
+ M.log_client_name = log_client_name
+end
+
+local function start_high_resolution_timer()
+ if M.use_high_resolution_time then
+ M.start_nanos = socket.gettime()
+ end
+end
+
+local function get_level_name(level)
+ for k, v in pairs(levels) do
+ if v == level then
+ return k
+ end
+ end
+ error("E-LOG-1: Unable to determine log level name for level number " .. level .. ".")
+end
+
+---
+-- Open a connection to a remote log receiver.
+--
+-- This method allows connecting the log to an external process listening on a TCP port. The process can be on a remote
+-- host. If the connection cannot be established, the logger falls back to console logging.
+--
+--
+-- @param host remote host on which the logging process runs
+--
+-- @param port TCP port on which the logging process listens
+--
+function M.connect(host, port)
+ local tcp_socket = socket.tcp()
+ tcp_socket:settimeout(M.connection_timeout)
+ local ok, err = tcp_socket:connect(host, port)
+ local log_client_prefix = M.log_client_name and (M.log_client_name .. ": ") or ""
+ if ok then
+ M.socket_client = tcp_socket
+ M.info("%sConnected to log receiver listening on %s:%d with log level %s. Timezone is UTC%s.", log_client_prefix, host, port,
+ get_level_name(M.level), os.date("%z"))
+ else
+ print(log_client_prefix .. "W-LOG-2: Unable to open socket connection to " .. host .. ":" .. port
+ .. "for sending log messages. Falling back to console logging with log level " .. get_level_name(M.level)
+ .. ". Timezone is UTC" .. os.date("%z") .. ". Caused by: " .. err)
+ end
+end
+
+---
+-- Close the connection to the remote log receiver.
+--
+function M.disconnect()
+ if(M.socket_client) then
+ M.socket_client:close()
+ end
+end
+
+---
+-- Set the log level.
+--
+-- @param level_name name of the log level, one of: FATAL, ERROR, WARN, INFO, CONFIG, DEBUG, TRACE
+--
+function M.set_level(level_name)
+ local level = levels[level_name]
+ if level == nil then
+ M.warning('W-LOG-1: Attempt to set illegal log level "' .. level_name
+ .. ' Pick one of: NONE, FATAL, ERROR, WARN, INFO, CONFIG, DEBUG, TRACE. Falling back to level INFO.')
+ M.level = levels.INFO
+ else
+ M.level = level
+ end
+end
+
+---
+-- Write to a socket, print or discard the message.
+--
+-- If a socket connection is established, this method writes to that socket. Otherwise if the global print function
+-- exists (e.g. in a unit test) falls back to logging via print().
+--
+-- Exasol removed print() in it's Lua implementation, so there is no fallback on a real Exasol instance. You either use
+-- remote logging or messages are discarded immediately.
+--
+--
+-- @param level log level
+--
+-- @param message log message; otherwise used as format string if any variadic parameters follow
+--
+-- @param ... parameters to be inserted into formatted message (optional)
+--
+local function write(level, message, ...)
+ if not M.socket_client and print then
+ return
+ else
+ local entry
+ local formatted_message = (select('#', ...) > 0) and string.format(message, ...) or message
+ if M.use_high_resolution_time then
+ local current_millis = string.format("%07.3f", (socket.gettime() - M.start_nanos) * 1000)
+ entry = {
+ os.date(M.timestamp_pattern),
+ " (", current_millis, "ms) [", level , "]",
+ string.rep(" ", 7 - string.len(level)), formatted_message
+ }
+ else
+ entry = {
+ os.date(M.timestamp_pattern),
+ " [", level , "]",
+ string.rep(" ", 7 - string.len(level)), formatted_message
+ }
+ end
+ if M.socket_client then
+ entry[#entry + 1] = "\n"
+ M.socket_client:send(table.concat(entry))
+ else
+ if(print) then
+ print(table.concat(entry))
+ end
+ end
+ end
+end
+
+function M.fatal(...)
+ if M.level >= levels.FATAL then
+ write("FATAL", ...)
+ end
+end
+
+function M.error(...)
+ if M.level >= levels.ERROR then
+ write("ERROR", ...)
+ end
+end
+
+function M.warn(...)
+ if M.level >= levels.WARN then
+ write("WARN", ...)
+ end
+end
+
+function M.info(...)
+ if M.level >= levels.INFO then
+ write("INFO", ...)
+ end
+end
+
+function M.config(...)
+ if M.level >= levels.CONFIG then
+ write("CONFIG", ...)
+ end
+end
+
+function M.debug(...)
+ if M.level >= levels.DEBUG then
+ write("DEBUG", ...)
+ end
+end
+
+function M.trace(...)
+ if M.level >= levels.FATAL then
+ write("TRACE", ...)
+ end
+end
+
+start_high_resolution_timer()
+return M
diff --git a/src/main/lua/exasolrls/adapter.lua b/src/main/lua/exasolrls/adapter.lua
new file mode 100644
index 0000000..ccb4bf9
--- /dev/null
+++ b/src/main/lua/exasolrls/adapter.lua
@@ -0,0 +1,54 @@
+local metadata_reader = require("exasolrls.metadata_reader")
+local query_rewriter = require("exasolrls.query_rewriter")
+
+local M = {VERSION = "0.1.0", NAME = "Row-level Security adapter (LUA)"}
+
+---
+-- Create a virtual schema.
+--
+-- @param exa_metadata Exasol metadata
+--
+-- @param request virtual schema request
+--
+-- @return response containing the metadata for the virtual schema like table and column structure
+--
+function M.create_virtual_schema(exa_metadata, request)
+ local properties = request.schemaMetadataInfo.properties
+ local schema_metadata = metadata_reader.read(properties.SCHEMA_NAME)
+ return {type = "createVirtualSchema", schemaMetadata = schema_metadata}
+end
+
+---
+-- Drop the virtual schema
+--
+-- @param exa_metadata Exasol metadata
+--
+-- @param request virtual schema request
+--
+-- @return response confirming the request (otherwise empty)
+--
+function M.drop_virtual_schema(exa_metadata, request)
+ return {type = "dropVirtualSchema"}
+end
+
+function M.refresh(exa_metadata, request)
+end
+
+function M.set_properties(exa_metadata, request)
+end
+
+function M.get_capabilities(exa_metadata, request)
+ return {type = "getCapabilities",
+ capabilities = {"SELECTLIST_PROJECTION", "AGGREGATE_SINGLE_GROUP", "AGGREGATE_GROUP_BY_COLUMN",
+ "AGGREGATE_GROUP_BY_TUPLE", "AGGREGATE_HAVING", "ORDER_BY_COLUMN", "LIMIT",
+ "LIMIT_WITH_OFFSET"}}
+end
+
+function M.push_down(exa_metadata, request)
+ local properties = request.schemaMetadataInfo.properties
+ local adapter_cache = request.schemaMetadataInfo.adapterNotes
+ local rewritten_query = query_rewriter.rewrite(request.pushdownRequest, properties.SCHEMA_NAME, adapter_cache)
+ return {type = "pushdown", sql = rewritten_query}
+end
+
+return M
\ No newline at end of file
diff --git a/src/main/lua/exasolrls/metadata_reader.lua b/src/main/lua/exasolrls/metadata_reader.lua
new file mode 100644
index 0000000..6a3aa1a
--- /dev/null
+++ b/src/main/lua/exasolrls/metadata_reader.lua
@@ -0,0 +1,77 @@
+local log = require("exasollog.log")
+local cjson require("cjson")
+
+M = {}
+
+local CONTROL_TABLES = {"EXA_RLS_USERS", "EXA_ROLE_MAPPING", "EXA_GROUP_MAPPING"}
+
+local function open_schema(schema_id)
+ local ok, result = exa.pquery('OPEN SCHEMA "' .. schema_id .. '"')
+ if not ok then
+ error("E-MDR-1: Unable to open source schema " .. schema_id .. " for reading metadata. Caused by: "
+ .. result.error_message)
+ end
+end
+
+local function read_columns(table_id)
+ local ok, result = exa.pquery('DESCRIBE "' .. table_id .. '"')
+ local columns = {}
+ local tenant_protected, role_protected, group_protected
+ if ok then
+ for i = 1, #result do
+ local column_id = result[i].COLUMN_NAME
+ if(column_id == "EXA_ROW_TENANT") then
+ tenant_protected = true
+ elseif(column_id == "EXA_ROW_ROLES") then
+ role_protected = true
+ elseif(column_id == "EXA_ROW_GROUP") then
+ group_protected = true
+ else
+ local column_type = result[i].SQL_TYPE
+ columns[i] = {name = column_id, dataType = {type = column_type}}
+ end
+ end
+ return columns, tenant_protected, role_protected, group_protected
+ else
+ error("E-MDR-3: Unable to read column metadata from source table " .. table_id .. ". Caused by: "
+ .. result.error_message)
+ end
+end
+
+local function read_tables(schema_id)
+ local ok, result = exa.pquery('SELECT "TABLE_NAME" FROM "CAT"')
+ local tables = {}
+ local table_protection = {}
+ if ok then
+ for i = 1, #result do
+ local table_id = result[i].TABLE_NAME
+ if (table_id ~= "EXA_RLS_USERS") and (table_id ~= "EXA_ROLE_MAPPING") and (table_id ~= "EXA_GROUP_MEMBERS")
+ then
+ local columns, tenant_protected, role_protected, group_protected = read_columns(table_id)
+ tables[i] = {name = table_id, columns = columns}
+ local protection = (tenant_protected and "t" or "-") .. (role_protected and "r" or "-")
+ .. (group_protected and "g" or "-")
+ log.debug('Found table "' .. table_id .. '" (' .. #columns .. ' columns). Protection: ' .. protection)
+ table.insert(table_protection, table_id .. ":" .. protection)
+ end
+ end
+ return tables, table_protection
+ else
+ error("E-MDR-2: Unable to read table metadata from source schema. Caused by: " .. result.error_message)
+ end
+end
+
+---
+-- Read the database metadata of the given schema (i.e. the internal structure of that schema)
+--
+-- @param schema schema to be scanned
+--
+-- @return schema metadata
+--
+function M.read(schema_id)
+ open_schema(schema_id)
+ local tables, table_protection = read_tables(schema_id)
+ return {tables = tables, adapterNotes = table.concat(table_protection, ",")}
+end
+
+return M
diff --git a/src/main/lua/exasolrls/query_rewriter.lua b/src/main/lua/exasolrls/query_rewriter.lua
new file mode 100644
index 0000000..98c0573
--- /dev/null
+++ b/src/main/lua/exasolrls/query_rewriter.lua
@@ -0,0 +1,53 @@
+local renderer = require("exasolvs.query_renderer")
+local protection = require("exasolrls.table_protection_status")
+local log = require("exasollog.log")
+
+local M = {}
+
+local function validate(query)
+ if not query then
+ error("E-RLS-QRW-1: Unable to rewrite query because it was .")
+ end
+ local push_down_type = query.type
+ if(push_down_type ~= "select") then
+ error('E-RLS-QRW-2: Unable to rewrite push-down request of type "' .. push_down_type
+ .. '". Only SELECT is supported.')
+ end
+end
+
+---
+-- Rewrite the original query with RLS restrictions.
+--
+-- @param original_query structure containing the original push-down query
+--
+-- @param sourceSchema source schema RLS is put on top of
+--
+-- @param adapter_cache cache taken from the adapter notes
+--
+-- @return string containing the rewritten query
+--
+function M.rewrite(original_query, source_schema, adapter_cache)
+ validate(original_query)
+ local query = original_query
+ local table = query.from.name
+ query.from.schema = source_schema
+ if protection.is_table_protected(source_schema, table, adapter_cache) then
+ log.debug('Table "%s" is RLS-protected. Adding row filters.', table)
+ local protection_filter = {
+ type = "predicate_equal",
+ left = {type = "column", tableName = table, name = "EXA_ROW_TENANT"},
+ right = {type = "function_scalar", name = "CURRENT_USER"}
+ }
+ local original_filter = query.filter
+ if original_filter then
+ query.filter = {type = "predicate_and", expressions = {protection_filter, original_filter}}
+ else
+ query.filter = protection_filter
+ end
+ else
+ log.debug('Table "%s" is not protected. No filters added.', table)
+ end
+ return renderer.new(query).render()
+end
+
+return M
\ No newline at end of file
diff --git a/src/main/lua/exasolrls/table_protection_status.lua b/src/main/lua/exasolrls/table_protection_status.lua
new file mode 100644
index 0000000..2da285f
--- /dev/null
+++ b/src/main/lua/exasolrls/table_protection_status.lua
@@ -0,0 +1,52 @@
+local log = require("exasollog.log")
+
+local M = {}
+
+local function determine_table_protection_from_cache(table_id, protection_cache)
+ local protection = string.match(protection_cache, table_id .. ":([-rtg]+)")
+ if not protection then
+ error("F-RLS-TPS-2: Unable to find table \"" .. table_id
+ .. "\" in protection status cache. Please check the table name and refresh the Virtual Schema.")
+ end
+ return protection ~= "---"
+end
+
+local function determine_table_protection_from_metadata(schema_id, table_id)
+ local fully_qualified_table_id = '"' .. schema_id .. '"."'.. table_id .. '"'
+ log.debug("Reading protection status of table %s.", fully_qualified_table_id)
+ local ok, result = exa.pquery('DESCRIBE ' .. fully_qualified_table_id)
+ if ok then
+ for i = 1, #result do
+ local column_name = result[i].COLUMN_NAME
+ if column_name == "EXA_ROW_TENANT" then
+ return true
+ end
+ end
+ return false
+ else
+ error('F-RLS-TPS-1: Unable to determine protection status of table "' .. table
+ .. "'. Metadata could not be read. Caused by: " + result.error_msg)
+ end
+end
+
+---
+-- Check whether a table is protected by RLS or not.
+--
+-- @param schema_id name of the schema the table belongs to
+--
+-- @param table_id name of the table for which to check the protection status
+--
+-- @param adapter_cache if present, used as cached source for determining the protection status of tables
+--
+-- @return true if the table is protected.
+--
+function M.is_table_protected(schema_id, table_id, adapter_cache)
+ if not adapter_cache then
+ return determine_table_protection_from_metadata(schema_id, table_id)
+ else
+ return determine_table_protection_from_cache(table_id, adapter_cache)
+ end
+end
+
+
+return M
\ No newline at end of file
diff --git a/src/main/lua/exasolvs/query_renderer.lua b/src/main/lua/exasolvs/query_renderer.lua
new file mode 100644
index 0000000..07fad85
--- /dev/null
+++ b/src/main/lua/exasolvs/query_renderer.lua
@@ -0,0 +1,177 @@
+local M = {}
+
+-- TODO: Move to a separate module!
+--
+function string.starts_with(text, start)
+ return start == string.sub(text, 1, string.len(start))
+end
+
+---
+-- Create a new query renderer.
+--
+-- @param query query to be rendered
+--
+-- @return new query renderer instance
+--
+function M.new (query)
+ local self = {original_query = query, query_elements = {}}
+ local OPERATORS = {
+ predicate_equal = "=", predicate_less = "<", predicate_greater = ">",
+ predicate_and = "AND", predicate_or = "OR", predicate_not = "NOT"
+ }
+
+ -- forward declarations
+ local append_unary_predicate, append_binary_predicate, append_iterated_predicate, append_expression
+
+ local function append(value)
+ self.query_elements[#self.query_elements + 1] = value
+ end
+
+ local function comma(index)
+ if index > 1 then
+ self.query_elements[#self.query_elements + 1] = ", "
+ end
+ end
+
+ local function append_column_reference(column)
+ append('"')
+ append(column.tableName)
+ append('"."')
+ append(column.name)
+ append('"')
+ end
+
+ local function append_scalar_function(scalar_function)
+ local function_name = scalar_function.name
+ append(function_name)
+ if function_name ~= "CURRENT_USER" then
+ append("(")
+ local arguments = scalar_function.arguments
+ if(arguments) then
+ for i = 1, #arguments do
+ comma(i)
+ append_expression(arguments[i])
+ end
+ end
+ append(")")
+ end
+ end
+
+ local function append_select_list_elements(select_list)
+ for i = 1, #select_list do
+ local element = select_list[i]
+ local type = element.type
+ comma(i)
+ append_expression(element)
+ end
+ end
+
+ local function append_select_list()
+ local select_list = self.original_query.selectList
+ if not select_list then
+ append("*")
+ else
+ append_select_list_elements(select_list)
+ end
+ end
+
+ local function append_from()
+ if self.original_query.from then
+ append(' FROM "')
+ if self.original_query.from.schema then
+ append(self.original_query.from.schema)
+ append('"."')
+ end
+ append(self.original_query.from.name)
+ append('"')
+ end
+ end
+
+ local function append_predicate(operand)
+ local type = string.sub(operand.type, 11)
+ if type == "equal" or type == "greater" or type == "less" then
+ append_binary_predicate(operand)
+ elseif type == "not" then
+ append_unary_predicate(operand)
+ elseif type == "and" or type == "or" then
+ append_iterated_predicate(operand)
+ else
+ error('E-VS-QR-2: Unable to render unknown SQL predicate type "' .. type .. '".')
+ end
+ end
+
+ append_expression = function (expression)
+ local type = expression.type
+ if type == "column" then
+ append_column_reference(expression)
+ elseif(type == "literal_exactnumeric" or type == "literal_boolean") then
+ append(expression.value)
+ elseif(type == "literal_string") then
+ append("'")
+ append(expression.value)
+ append("'")
+ elseif(type == "function_scalar") then
+ append_scalar_function(expression)
+ elseif(string.starts_with(type, "predicate_")) then
+ append_predicate(expression)
+ else
+ error('E-VS-QR-1: Unable to render unknown SQL expression type "' .. expression.type .. '".')
+ end
+ end
+
+ append_unary_predicate = function (predicate)
+ local type = predicate.type
+ append("(")
+ append(OPERATORS[predicate.type])
+ append(" ")
+ append_expression(predicate.expression)
+ append(")")
+ end
+
+ append_binary_predicate = function (predicate)
+ append("(")
+ append_expression(predicate.left)
+ append(" ")
+ append(OPERATORS[predicate.type])
+ append(" ")
+ append_expression(predicate.right)
+ append(")")
+ end
+
+ append_iterated_predicate = function (predicate)
+ append("(")
+ local expressions = predicate.expressions
+ for i = 1, #expressions do
+ if i > 1 then
+ append(" ")
+ append(OPERATORS[predicate.type])
+ append(" ")
+ end
+ append_expression(expressions[i])
+ end
+ append(")")
+ end
+
+ local function append_filter()
+ if self.original_query.filter then
+ append(" WHERE ")
+ append_predicate(self.original_query.filter)
+ end
+ end
+
+ --- Render the query to a string.
+ --
+ -- @return query as string
+ --
+ local function render()
+ append("SELECT ")
+ append_select_list()
+ append_from()
+ append_filter()
+ return table.concat(self.query_elements, "")
+ end
+
+ return {render = render}
+end
+
+return M
diff --git a/src/main/lua/exasolvs/request_dispatcher.lua b/src/main/lua/exasolvs/request_dispatcher.lua
new file mode 100644
index 0000000..1371bd8
--- /dev/null
+++ b/src/main/lua/exasolvs/request_dispatcher.lua
@@ -0,0 +1,65 @@
+local log = require("exasollog.log")
+local cjson = require("cjson")
+local adapter = require("exasolrls.adapter", "adapter")
+
+local function handle_request(request)
+ local handlers = {
+ pushdown = adapter.push_down,
+ createVirtualSchema = adapter.create_virtual_schema,
+ dropVirtualSchema = adapter.drop_virtual_schema,
+ refresh = adapter.refresh,
+ getCapabilities = adapter.get_capabilities,
+ setProperties = adapter.set_properties
+ }
+ log.info('Received "%s" request.', request.type)
+ local handler = handlers[request.type]
+ if(handler ~= nil) then
+ local response = cjson.encode(handler(nil, request))
+ log.debug("Response:\n" .. response)
+ return response
+ else
+ error('F-RQD-1: Unknown Virtual Schema request type "%s" received.', request.type)
+ end
+end
+
+local function log_error(message)
+ local error_type = string.sub(message, 1, 2)
+ if(error_type == "F-") then
+ log.fatal(message)
+ else
+ log.error(message)
+ end
+end
+
+---
+-- RLS adapter entry point.
+--
+-- This global function receives the request from the Exasol core database.
+--
+--
+function adapter_call(request_as_json)
+ log.set_client_name(adapter.NAME .. " " .. adapter.VERSION)
+ local request = cjson.decode(request_as_json)
+ local properties = (request.schemaMetadataInfo or {}).properties or {}
+ local log_level = properties.LOG_LEVEL
+ if(log_level) then
+ log.set_level(string.upper(log_level))
+ end
+ local debug_address = properties.DEBUG_ADDRESS
+ if(debug_address) then
+ local colon_position = string.find(debug_address,":", 1, true)
+ local host = string.sub(debug_address, 1, colon_position - 1)
+ local port = string.sub(debug_address, colon_position + 1)
+ log.connect(host, port)
+ end
+ log.debug("Raw request:\n%s", request_as_json)
+ local ok, result = pcall(function () return handle_request(request) end)
+ if(ok) then
+ log.disconnect()
+ return result
+ else
+ log_error(result)
+ log.disconnect()
+ error(result)
+ end
+end
diff --git a/src/test/java/com/exasol/InstallLuaAdapterScript.java b/src/test/java/com/exasol/InstallLuaAdapterScript.java
new file mode 100644
index 0000000..200f858
--- /dev/null
+++ b/src/test/java/com/exasol/InstallLuaAdapterScript.java
@@ -0,0 +1,124 @@
+package com.exasol;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.logging.Logger;
+
+import com.exasol.dbbuilder.AdapterScript.Language;
+import com.exasol.dbbuilder.DatabaseObjectFactory;
+import com.exasol.dbbuilder.ExasolObjectFactory;
+import com.exasol.dbbuilder.Schema;
+
+/**
+ * This class contains an installation helper intended to simplify declaring Lua adapter scripts.
+ *
+ * This script is intended for integration tests of Lua adapter scripts. It is a quick way to get the adapter script
+ * installed.
+ *
+ *
+ * IMPORTANT: This script creates a fresh schema around the adapter each time. Any other objects in that schema
+ * will be lost!
+ *
+ */
+public class InstallLuaAdapterScript {
+ private static final String USAGE = "Usage: java InstallLuaAdapterScript.java \n"
+ + " [] [] []";
+ private static final Logger LOGGER = Logger.getLogger(InstallLuaAdapterScript.class.getName());
+ private static final String EXASOL_LUA_MODULE_LOADER_WORKAROUND = "table.insert(package.loaders,\n" //
+ + " function (module_name)\n" //
+ + " local loader = package.preload[module_name]\n" //
+ + " if not loader then\n" //
+ + " error(\"Module \" .. module_name .. \" not found in package.preload.\")\n" //
+ + " else\n" //
+ + " return loader\n" //
+ + " end\n" //
+ + " end\n" //
+ + ")\n\n";
+ private static final String DEFAULT_CONNECTION_STRING = "jdbc:exa:127.0.0.1:8563";
+ private static final String DEFAULT_DATABASE_USER = "SYS";
+ private static final String DEFAULT_DATABASE_PWD = "exasol";
+ private final String adapterName;
+ private final String adapterSchemaName;
+ private Connection connection;
+ private final String password;
+ private final Path scriptPath;
+ private final String connectionString;
+ private final String user;
+
+ /**
+ * Entry point for the Lua adapter script installer.
+ *
+ * Note that the adapter schema should only be used for this adapter. The installer will remove an existing schema
+ * of that name and recreate it.
+ *
+ *
+ * @param arguments array of command line arguments
+ *
+ *
name of the schema that the adapter will be installed in
+ *
adapter script name
+ *
path to script
+ *
database connection string (optional, defaults to "jdbc:exa:127.0.0.1:8563")
+ *
database user (optional, defaults to "SYS")
+ *
database password (optional, defaults to "exasol")
+ *
+ */
+ public static void main(final String[] arguments) {
+ if (arguments.length < 3) {
+ System.out.println(USAGE);
+ System.exit(-1);
+ }
+ final String adapterSchemaName = arguments[0];
+ final String adapterScriptName = arguments[1];
+ final Path scriptPath = Path.of(arguments[2]);
+ final String connectionString = getOptionalArgument(arguments, 3, DEFAULT_CONNECTION_STRING);
+ final String user = getOptionalArgument(arguments, 4, DEFAULT_DATABASE_USER);
+ final String password = getOptionalArgument(arguments, 5, DEFAULT_DATABASE_PWD);
+ new InstallLuaAdapterScript(adapterSchemaName, adapterScriptName, scriptPath, connectionString, user, password)
+ .run();
+ }
+
+ private static String getOptionalArgument(final String arguments[], final int index, final String defaultValue) {
+ return (arguments.length > index) && (arguments[index] != null) ? arguments[index] : defaultValue;
+ }
+
+ private InstallLuaAdapterScript(final String adapterSchemaName, final String adapterName, final Path scriptPath,
+ final String connectionString, final String user, final String password) {
+ this.adapterName = adapterName;
+ this.adapterSchemaName = adapterSchemaName;
+ this.scriptPath = scriptPath;
+ this.connectionString = connectionString;
+ this.user = user;
+ this.password = password;
+ }
+
+ private void run() {
+ try {
+ this.connection = DriverManager.getConnection(this.connectionString, this.user, this.password);
+ final DatabaseObjectFactory factory = new ExasolObjectFactory(this.connection);
+ cleanUpOldEntries();
+ final Schema scriptSchema = factory.createSchema(this.adapterSchemaName);
+ installAdapter(scriptSchema, this.scriptPath);
+ } catch (final SQLException | IOException exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ private void cleanUpOldEntries() throws SQLException {
+ final Statement statement = this.connection.createStatement();
+// statement.execute(
+// "DROP ADAPTER SCRIPT IF EXISTS \"" + this.adapterSchemaName + "\".\"" + this.adapterName + "\"");
+ statement.execute("DROP SCHEMA IF EXISTS \"" + this.adapterSchemaName + "\" CASCADE");
+ }
+
+ private void installAdapter(final Schema scriptSchema, final Path scriptContent) throws IOException {
+ LOGGER.info(() -> "Installing adpater script \"" + this.adapterName + "\" in schema \"" + scriptSchema.getName()
+ + "\".");
+ final String content = EXASOL_LUA_MODULE_LOADER_WORKAROUND + Files.readString(scriptContent);
+ scriptSchema.createAdapterScript(this.adapterName, Language.LUA, content);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/RequestDispatcherIT.java b/src/test/java/com/exasol/RequestDispatcherIT.java
new file mode 100644
index 0000000..5adcddc
--- /dev/null
+++ b/src/test/java/com/exasol/RequestDispatcherIT.java
@@ -0,0 +1,102 @@
+package com.exasol;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.JdbcDatabaseContainer.NoDriverFoundException;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import com.exasol.containers.ExasolContainer;
+import com.exasol.dbbuilder.AdapterScript;
+import com.exasol.dbbuilder.AdapterScript.Language;
+import com.exasol.dbbuilder.ExasolObjectFactory;
+import com.exasol.dbbuilder.ObjectPrivilege;
+import com.exasol.dbbuilder.Schema;
+import com.exasol.dbbuilder.User;
+import com.exasol.dbbuilder.VirtualSchema;
+import com.exasol.matcher.ResultSetStructureMatcher;
+
+@Testcontainers
+class RequestDispatcherIT {
+ private static final Path RLS_PACKAGE_PATH = Path.of("target/row-level-security-dist-0.1.0.lua");
+ @Container
+ private static ExasolContainer extends ExasolContainer>> container = new ExasolContainer<>(
+ "exasol/docker-db:7.0.rc1-d1") //
+ .withRequiredServices() //
+ .withExposedPorts(8563);
+ private static final String EXASOL_LUA_MODULE_LOADER_WORKAROUND = "table.insert(package.loaders,\n" //
+ + " function (module_name)\n" //
+ + " local loader = package.preload[module_name]\n" //
+ + " if(loader == nil) then\n" //
+ + " error(\"Module \" .. module_name .. \" not found in package.preload.\")\n" //
+ + " else\n" //
+ + " return loader\n" //
+ + " end\n" //
+ + " end\n" //
+ + ")\n\n";
+ private static Connection connection;
+ private static ExasolObjectFactory factory;
+ private static Schema scriptSchema;
+
+ @BeforeAll
+ static void beforeAll() throws NoDriverFoundException, SQLException {
+ connection = container.createConnection("");
+ factory = new ExasolObjectFactory(connection);
+ scriptSchema = factory.createSchema("L");
+ }
+
+ @Test
+ void testUnprotected() throws IOException, SQLException {
+ final String sourceSchemaName = "UNPROTECTED";
+ final Schema sourceSchema = factory.createSchema(sourceSchemaName);
+ sourceSchema.createTable("T", "C1", "BOOLEAN") //
+ .insert("true") //
+ .insert("false");
+ final VirtualSchema virtualSchema = createVirtualSchema(sourceSchema);
+ final User user = factory.createLoginUser("UP_USER").grant(virtualSchema, ObjectPrivilege.SELECT);
+ assertThat(executeRlsQueryWithUser("SELECT C1 FROM " + sourceSchemaName + "_RLS.T", user),
+ ResultSetStructureMatcher.table("BOOLEAN").row(true).row(false).matches());
+ }
+
+ private VirtualSchema createVirtualSchema(final Schema sourceSchema) throws IOException {
+ final String name = sourceSchema.getName();
+ final String content = EXASOL_LUA_MODULE_LOADER_WORKAROUND + Files.readString(RLS_PACKAGE_PATH);
+ final AdapterScript adapterScript = scriptSchema.createAdapterScript(name + "_ADAPTER", Language.LUA, content);
+ return factory.createVirtualSchemaBuilder(name + "_RLS") //
+ .adapterScript(adapterScript) //
+ .sourceSchema(sourceSchema) //
+ .properties(Map.of("LOG_LEVEL", "TRACE", "DEBUG_ADDRESS", "172.17.0.1:3000")).build();
+ }
+
+ private ResultSet executeRlsQueryWithUser(final String query, final User user) throws SQLException {
+ final Statement statement = container.createConnectionForUser(user.getName(), user.getPassword())
+ .createStatement();
+ final ResultSet result = statement.executeQuery(query);
+ return result;
+ }
+
+ @Test
+ void testTenantProtected() throws IOException, SQLException {
+ final String sourceSchemaName = "TENANT_PROTECTED";
+ final Schema sourceSchema = factory.createSchema(sourceSchemaName);
+ sourceSchema.createTable("T", "C1", "BOOLEAN", "C2", "DATE", "EXA_ROW_TENANT", "VARCHAR(128)") //
+ .insert("false", "2020-01-01", "NON_TENANT_USER") //
+ .insert("true", "2020-02-02", "TENANT_USER");
+ final VirtualSchema virtualSchema = createVirtualSchema(sourceSchema);
+ final User user = factory.createLoginUser("TENANT_USER").grant(virtualSchema, ObjectPrivilege.SELECT);
+ factory.createLoginUser("NON_TENANT_USER").grant(virtualSchema, ObjectPrivilege.SELECT);
+ assertThat(executeRlsQueryWithUser("SELECT C1 FROM " + sourceSchemaName + "_RLS.T", user),
+ ResultSetStructureMatcher.table("BOOLEAN").row(true).matches());
+ }
+}
\ No newline at end of file
diff --git a/src/test/lua/exasollog/test_log.lua b/src/test/lua/exasollog/test_log.lua
new file mode 100644
index 0000000..163c796
--- /dev/null
+++ b/src/test/lua/exasollog/test_log.lua
@@ -0,0 +1,94 @@
+local luaunit = require("luaunit")
+local mockagne = require("mockagne")
+
+local when, verify, any = mockagne.when, mockagne.verify, mockagne.any
+local date_pattern = "%Y-%m-%d"
+
+test_log = {}
+
+function test_log:setUp()
+ self.today = os.date(date_pattern)
+ self.socket_mock = mockagne.getMock()
+ self.tcp_mock = mockagne.getMock()
+ self.client_mock = self.tcp_mock -- on connect(), the socket library promotes a TCP socket to a client socket
+ when(self.socket_mock.tcp()).thenAnswer(self.tcp_mock)
+ when(self.tcp_mock:connect(any(), any())).thenAnswer(1)
+ when(self.socket_mock.gettime()).thenAnswer(1000000)
+ package.preload["socket"] = function () return self.socket_mock end
+ self.log = require("exasollog.log").init(date_pattern, false)
+ self.log.set_client_name("Unit test")
+ self.log.connect("localhost", 3000)
+end
+
+function test_log:tearDown()
+ self.log.disconnect()
+ package.loaded["socket"] = nil
+ package.loaded["exasollog.log"] = nil
+end
+
+function test_log:assert_message(message)
+ verify(self.client_mock:send(message))
+end
+
+function test_log:assert_no_message(message)
+ luaunit.assertErrorMsgContains("no invocation made", function() self:assert_message(message) end)
+end
+
+function test_log:test_startup_message()
+ local timezone = os.date("%z")
+ self:assert_message(self.today .. " [INFO] Unit test: Connected to log receiver listening on localhost:3000"
+ .. " with log level INFO. Timezone is UTC" .. timezone .. ".\n")
+end
+
+function test_log:test_fatal()
+ self.log.fatal("Good by, cruel world!")
+ self:assert_message(self.today .. " [FATAL] Good by, cruel world!\n")
+end
+
+function test_log:test_error()
+ self.log.error("Oops!")
+ self:assert_message(self.today .. " [ERROR] Oops!\n")
+end
+
+function test_log:test_warn()
+ self.log.warn("This looks suspicious...")
+ self:assert_message(self.today .. " [WARN] This looks suspicious...\n")
+end
+
+function test_log:test_info()
+ self.log.info("Good to know.")
+ self:assert_message(self.today .. " [INFO] Good to know.\n")
+end
+
+function test_log:test_config()
+ self.log.set_level("CONFIG")
+ self.log.config("Life support enabled.")
+ self:assert_message(self.today .. " [CONFIG] Life support enabled.\n")
+end
+
+function test_log:test_debug()
+ self.log.set_level("DEBUG")
+ self.log.debug("Look what we have here.")
+ self:assert_message(self.today .. " [DEBUG] Look what we have here.\n")
+end
+
+function test_log:test_trace()
+ self.log.set_level("TRACE")
+ self.log.trace("foo(bar)")
+ self:assert_message(self.today .. " [TRACE] foo(bar)\n")
+end
+
+function test_log:test_set_log_level()
+ self.log.set_level("WARN")
+ self.log.info("don't send")
+ self.log.warn("send")
+ self:assert_message(self.today .. " [WARN] send\n")
+ self:assert_no_message(self.today .. " [INFO] don't send\n")
+end
+
+function test_log:test_logging_with_format_string()
+ self.log.info('%s says "Mount Everest is %d meters high."', "Simon", 8848)
+ self:assert_message(self.today .. ' [INFO] Simon says "Mount Everest is 8848 meters high."\n')
+end
+
+os.exit(luaunit.LuaUnit.run())
diff --git a/src/test/lua/exasolrls/test_adapter.lua b/src/test/lua/exasolrls/test_adapter.lua
new file mode 100644
index 0000000..42b00f2
--- /dev/null
+++ b/src/test/lua/exasolrls/test_adapter.lua
@@ -0,0 +1,35 @@
+local luaunit = require("luaunit")
+
+local mockagne = require("mockagne")
+local metadata_reader_mock = mockagne.getMock()
+package.preload["exasolrls.metadata_reader"] = function () return metadata_reader_mock end
+
+local adapter = require("exasolrls.adapter")
+
+test_rls_adapter = {}
+
+function test_rls_adapter:test_drop_virtual_schema()
+ luaunit.assertEquals(adapter.drop_virtual_schema(), {type="dropVirtualSchema"})
+end
+
+function test_rls_adapter.test_create_virtual_schema()
+ local schema_metadata = {tables = {{type = "table", name = "T1", columns =
+ {{name = "C1", dataType = { type = "BOOLEAN"}}}}}}
+ mockagne.when(metadata_reader_mock.read("S")).thenAnswer(schema_metadata)
+ local expected = {type = "createVirtualSchema",
+ schemaMetadata = schema_metadata}
+ local request = {schemaMetadataInfo = {name = "V", properties = {SCHEMA_NAME = "S"}}}
+ local actual = adapter.create_virtual_schema(nil, request)
+ luaunit.assertEquals(actual, expected)
+end
+
+function test_rls_adapter:test_get_capabilites()
+ local expected = {type = "getCapabilities",
+ capabilities = {"SELECTLIST_PROJECTION", "AGGREGATE_SINGLE_GROUP", "AGGREGATE_GROUP_BY_COLUMN",
+ "AGGREGATE_GROUP_BY_TUPLE", "AGGREGATE_HAVING", "ORDER_BY_COLUMN", "LIMIT",
+ "LIMIT_WITH_OFFSET"}}
+ local actual = adapter.get_capabilities()
+ luaunit.assertEquals(actual , expected)
+end
+
+os.exit(luaunit.LuaUnit.run())
diff --git a/src/test/lua/exasolrls/test_metadata_reader.lua b/src/test/lua/exasolrls/test_metadata_reader.lua
new file mode 100644
index 0000000..4d4f98f
--- /dev/null
+++ b/src/test/lua/exasolrls/test_metadata_reader.lua
@@ -0,0 +1,49 @@
+local luaunit = require("luaunit")
+local mockagne = require("mockagne")
+local reader = require("exasolrls.metadata_reader")
+
+test_metadata_reader = {}
+
+function test_metadata_reader.test_read()
+ local exa_mock = mockagne.getMock()
+ _G.exa = exa_mock
+ mockagne.when(exa_mock.pquery('OPEN SCHEMA "S"')).thenAnswer(true)
+ mockagne.when(exa_mock.pquery('SELECT "TABLE_NAME" FROM "CAT"')).thenAnswer(true, {{TABLE_NAME = "T1"}})
+ mockagne.when(exa_mock.pquery('DESCRIBE "T1"')).thenAnswer(true, {{COLUMN_NAME = "C1", SQL_TYPE = "BOOLEAN"}})
+ luaunit.assertEquals(reader.read("S"),
+ {tables = {{name = "T1", columns = {{name = "C1", dataType = {type = "BOOLEAN"}}}}},
+ adapterNotes="T1:---"
+ })
+end
+
+function test_metadata_reader.test_hide_control_tables()
+ local exa_mock = mockagne.getMock()
+ _G.exa = exa_mock
+ mockagne.when(exa_mock.pquery('OPEN SCHEMA "S"')).thenAnswer(true)
+ mockagne.when(exa_mock.pquery('SELECT "TABLE_NAME" FROM "CAT"'))
+ .thenAnswer(true, {{TABLE_NAME = "T2"}, {TABLE_NAME = "EXA_RLS_USERS"}, {TABLE_NAME = "EXA_ROLE_MAPPING"},
+ {TABLE_NAME = "EXA_GROUP_MEMBERS"}})
+ mockagne.when(exa_mock.pquery('DESCRIBE "T2"')).thenAnswer(true, {{COLUMN_NAME = "C2", SQL_TYPE = "DATE"}})
+ luaunit.assertEquals(reader.read("S"),
+ {tables = {{name = "T2", columns = {{name = "C2", dataType = {type = "DATE"}}}}}, adapterNotes="T2:---"})
+end
+
+function test_metadata_reader.test_hide_control_columns()
+ local exa_mock = mockagne.getMock()
+ _G.exa = exa_mock
+ mockagne.when(exa_mock.pquery('OPEN SCHEMA "S"')).thenAnswer(true)
+ mockagne.when(exa_mock.pquery('SELECT "TABLE_NAME" FROM "CAT"'))
+ .thenAnswer(true, {{TABLE_NAME = "T3"}, {TABLE_NAME = "T4"}})
+ mockagne.when(exa_mock.pquery('DESCRIBE "T3"'))
+ .thenAnswer(true, {{COLUMN_NAME = "C3_1", SQL_TYPE = "BOOLEAN"}, {COLUMN_NAME = "EXA_ROW_TENANT"},
+ {COLUMN_NAME = "EXA_ROW_ROLES"}})
+ mockagne.when(exa_mock.pquery('DESCRIBE "T4"'))
+ .thenAnswer(true, {{COLUMN_NAME = "C4_1", SQL_TYPE = "DATE"}, {COLUMN_NAME = "EXA_ROW_GROUP"}})
+ luaunit.assertEquals(reader.read("S"),
+ {tables = {
+ {name = "T3", columns = {{name = "C3_1", dataType = {type = "BOOLEAN"}}}},
+ {name = "T4", columns = {{name = "C4_1", dataType = {type = "DATE"}}}}
+ }, adapterNotes = "T3:tr-,T4:--g" })
+end
+
+os.exit(luaunit.LuaUnit.run())
diff --git a/src/test/lua/exasolrls/test_query_rewriter.lua b/src/test/lua/exasolrls/test_query_rewriter.lua
new file mode 100644
index 0000000..215da23
--- /dev/null
+++ b/src/test/lua/exasolrls/test_query_rewriter.lua
@@ -0,0 +1,33 @@
+local luaunit = require("luaunit")
+local rewriter = require("exasolrls.query_rewriter")
+
+test_query_rewriter = {}
+
+function assert_rewrite(original_query, expected)
+ luaunit.assertEquals(rewriter.rewrite(original_query), expected)
+end
+
+function test_query_rewriter.test_unprotected_table()
+ local original_query = {
+ type = "select",
+ selectList = {
+ {type = "column", name = "C1", tableName = "UNPROT"},
+ {type = "column", name = "C2", tableName = "UNPROT"}
+ },
+ from = { type = "table", name = "UNPROT"}
+ }
+ assert_rewrite(original_query, 'SELECT "UNPROT"."C1", "UNPROT"."C2" FROM "UNPROT"')
+end
+
+function test_query_rewriter.test_tenant_protected_table()
+ local original_query = {
+ type = "select",
+ selectList = {
+ {type = "column", name = "C1", tableName = "PROT"},
+ },
+ from = { type = "table", name = "PROT"}
+ }
+ assert_rewrite(original_query, 'SELECT "PROT"."C1" FROM "PROT" WHERE ("PROT"."EXA_ROW_TENANT" = CURRENT_USER())')
+end
+
+os.exit(luaunit.LuaUnit.run())
diff --git a/src/test/lua/exasolrls/test_table_protection_status.lua b/src/test/lua/exasolrls/test_table_protection_status.lua
new file mode 100644
index 0000000..1b671ed
--- /dev/null
+++ b/src/test/lua/exasolrls/test_table_protection_status.lua
@@ -0,0 +1,39 @@
+local luaunit = require("luaunit")
+
+local mockagne = require("mockagne")
+
+local protection = require("exasolrls.table_protection_status")
+
+test_table_protection_status = {}
+
+function test_table_protection_status.test_is_protected_calculated_from_cache_true()
+ local source_schema = "S"
+ local adapter_cache = "CITIES:---,PEOPLE:t--"
+ luaunit.assertEquals(protection.is_table_protected(source_schema, "PEOPLE", adapter_cache), true)
+ luaunit.assertEquals(protection.is_table_protected(source_schema, "CITIES", adapter_cache), false)
+end
+
+function test_table_protection_status.test_is_protected_false()
+ local source_schema = "S"
+ local table = "MONTHS"
+ local exa_mock = mockagne.getMock()
+ _G.exa = exa_mock
+ mockagne.when(exa_mock.pquery('DESCRIBE "' .. source_schema .. '"."' .. table .. '"'))
+ .thenAnswer(true, {{COLUMN_NAME = "C1", SQL_TYPE = "BOOLEAN"}})
+ luaunit.assertEquals(protection.is_table_protected(source_schema, table), false)
+end
+
+function test_table_protection_status.test_is_protected_true()
+ local source_schema = "S"
+ local table = "PEOPLE"
+ local exa_mock = mockagne.getMock()
+ _G.exa = exa_mock
+ mockagne.when(exa_mock.pquery('DESCRIBE "' .. source_schema .. '"."'.. table .. '"'))
+ .thenAnswer(true, {
+ {COLUMN_NAME = "C1", SQL_TYPE = "BOOLEAN"},
+ {COLUMN_NAME = "EXA_ROW_TENANT", SQL_TYPE = "VARCHAR"}
+ })
+ luaunit.assertEquals(protection.is_table_protected(source_schema, table), true)
+end
+
+os.exit(luaunit.LuaUnit.run())
diff --git a/src/test/lua/exasolvs/test_query_renderer.lua b/src/test/lua/exasolvs/test_query_renderer.lua
new file mode 100644
index 0000000..9cff80e
--- /dev/null
+++ b/src/test/lua/exasolvs/test_query_renderer.lua
@@ -0,0 +1,118 @@
+luaunit = require("luaunit")
+renderer = require("exasolvs.query_renderer")
+
+test_query_renderer = {}
+
+local function assert_renders_to(original_query, expected)
+ luaunit.assertEquals(renderer.new(original_query).render(), expected);
+end
+
+function test_query_renderer.test_render_simple_select()
+ local original_query = {
+ type = "select",
+ selectList = {
+ {type = "column", name = "C1", tableName = "T1"},
+ {type = "column", name = "C2", tableName = "T1"}
+ },
+ from = {type = "table", name = "T1"}
+ }
+ assert_renders_to(original_query, 'SELECT "T1"."C1", "T1"."C2" FROM "T1"');
+end
+
+function test_query_renderer.test_render_with_single_predicate_filter()
+ local original_query = {
+ type = "select",
+ selectList = {{type = "column", name ="NAME", tableName = "MONTHS"}},
+ from = {type = "table", name = "MONTHS"},
+ filter = {
+ type = "predicate_greater",
+ left = {type = "column", name="DAYS_IN_MONTH", tableName = "MONTHS"},
+ right = {type = "literal_exactnumeric", value = "30"}
+ }
+ }
+ assert_renders_to(original_query, 'SELECT "MONTHS"."NAME" FROM "MONTHS" WHERE ("MONTHS"."DAYS_IN_MONTH" > 30)');
+end
+
+function test_query_renderer.test_render_nested_predicate_filter()
+ local original_query = {
+ type = "select",
+ selectList = {{type = "column", name ="NAME", tableName = "MONTHS"}},
+ from = {type = "table", name = "MONTHS"},
+ filter = {
+ type = "predicate_and",
+ expressions = {{
+ type = "predicate_equal",
+ left = {type = "literal_string", value = "Q3"},
+ right = {type = "column", name="QUARTER", tableName = "MONTHS"}
+ }, {
+ type = "predicate_greater",
+ left = {type = "column", name="DAYS_IN_MONTH", tableName = "MONTHS"},
+ right = {type = "literal_exactnumeric", value = "30"}
+ }
+ }
+ }
+ }
+ assert_renders_to(original_query, 'SELECT "MONTHS"."NAME" FROM "MONTHS"'
+ .. ' WHERE ((\'Q3\' = "MONTHS"."QUARTER") AND ("MONTHS"."DAYS_IN_MONTH" > 30))');
+end
+
+function test_query_renderer.test_render_unary_not_filter()
+ local original_query = {
+ type = "select",
+ selectList = {{type = "column", name ="NAME", tableName = "MONTHS"}},
+ from = {type = "table", name = "MONTHS"},
+ filter = {
+ type = "predicate_not",
+ expression = {
+ type = "predicate_equal",
+ left = {type = "literal_string", value = "Q3"},
+ right = {type = "column", name="QUARTER", tableName = "MONTHS"}
+ },
+ }
+ }
+ assert_renders_to(original_query, 'SELECT "MONTHS"."NAME" FROM "MONTHS"'
+ .. ' WHERE (NOT (\'Q3\' = "MONTHS"."QUARTER"))');
+end
+
+function test_query_renderer.test_scalar_function_in_select_list()
+ local original_query = {
+ type = "select",
+ selectList = {
+ {type = "function_scalar", name ="UPPER", arguments = {{type = "literal_string", value = "bob"}}}
+ }
+ }
+ assert_renders_to(original_query, "SELECT UPPER('bob')")
+end
+
+function test_query_renderer.test_scalar_function_in_select_list()
+ local original_query = {
+ type = "select",
+ selectList = {
+ {type = "column", name = "LASTNAME", tableName = "PEOPLE"}
+ },
+ from = {type = "table", name = "PEOPLE"},
+ filter = {
+ type = "predicate_equal",
+ left = {
+ type = "function_scalar",
+ name = "LOWER",
+ arguments = {
+ {type = "column", name = "FIRSTNAME", tableName = "PEOPLE"},
+ }
+ },
+ right = {type = "literal_string", value = "eve"}
+ }
+ }
+ assert_renders_to(original_query, 'SELECT "PEOPLE"."LASTNAME" FROM "PEOPLE" WHERE (LOWER("PEOPLE"."FIRSTNAME") = \'eve\')')
+end
+
+function test_query_renderer.test_current_user()
+ local original_query = {
+ type = "select",
+ selectList = {{type = "function_scalar", name = "CURRENT_USER"}}
+ }
+ assert_renders_to(original_query, 'SELECT CURRENT_USER')
+
+end
+
+os.exit(luaunit.LuaUnit.run())
diff --git a/src/test/lua/exasolvs/test_request_dispatcher.lua b/src/test/lua/exasolvs/test_request_dispatcher.lua
new file mode 100644
index 0000000..ff35294
--- /dev/null
+++ b/src/test/lua/exasolvs/test_request_dispatcher.lua
@@ -0,0 +1,38 @@
+local luaunit = require("luaunit")
+local mockagne = require("mockagne")
+local log_mock = mockagne.getMock()
+package.preload["exasollog.log"] = function () return log_mock end
+local cjson = require("cjson")
+local dispatcher = require("exasolvs.request_dispatcher")
+
+local verify, when, any = mockagne.verify, mockagne.when, mockagne.any
+
+test_request_dispatcher = {}
+
+local function json_assert(actual, expected)
+ luaunit.assertEquals(cjson.decode(actual), expected)
+end
+
+function test_request_dispatcher:test_get_capabilities()
+ local response = adapter_call('{"type" : "getCapabilities"}')
+ local expected = {type = "getCapabilities", capabilities = {
+ "SELECTLIST_PROJECTION",
+ "AGGREGATE_SINGLE_GROUP",
+ "AGGREGATE_GROUP_BY_COLUMN",
+ "AGGREGATE_GROUP_BY_TUPLE",
+ "AGGREGATE_HAVING",
+ "ORDER_BY_COLUMN",
+ "LIMIT",
+ "LIMIT_WITH_OFFSET"
+ }}
+ json_assert(response, expected)
+end
+
+function test_request_dispatcher:test_setup_remote_logging()
+ adapter_call('{"type" : "getCapabilities", "schemaMetadataInfo" : '
+ .. '{"properties" : {"DEBUG_ADDRESS" : "10.0.0.1:4000", "LOG_LEVEL" : "TRACE"}}}')
+ verify(log_mock.set_level("TRACE"))
+ verify(log_mock.connect("10.0.0.1", "4000"))
+end
+
+os.exit(luaunit.LuaUnit.run())
diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties
new file mode 100644
index 0000000..ad3bc9a
--- /dev/null
+++ b/src/test/resources/logging.properties
@@ -0,0 +1,6 @@
+handlers=java.util.logging.ConsoleHandler
+.level=INFO
+java.util.logging.ConsoleHandler.level=ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+java.util.logging.SimpleFormatter.format=%1$tF %1$tT.%1$tL [%4$-7s] %5$s %n
+com.exasol=ALL
\ No newline at end of file
diff --git a/tools/amalg.lua b/tools/amalg.lua
new file mode 100644
index 0000000..7a915de
--- /dev/null
+++ b/tools/amalg.lua
@@ -0,0 +1,765 @@
+#!/usr/bin/env lua
+
+-- **Amalg** is a Lua tool for bundling a Lua script and dependent
+-- Lua modules in a single `.lua` file for easier distribution.
+--
+-- Features:
+-- * Pure Lua (compatible with Lua 5.1 and up), no external
+-- dependencies. (Even works for modules using the deprecated
+-- `module` function.)
+-- * You don't have to take care of the order in which the modules
+-- are `require`d.
+-- * Can embed compiled C modules.
+-- * Can collect `require`d Lua (and C) modules automatically.
+--
+-- What it doesn't do:
+--
+-- * It does not compile to bytecode. Use `luac` for that yourself,
+-- or take a look at [squish][1], or [luac.lua][4].
+-- * It doesn't do static analysis of Lua code to collect `require`d
+-- modules. That won't work reliably anyway in a dynamic language!
+-- You can write your own program for that (e.g. using the output
+-- of `luac -p -l`), or use [squish][1], or [soar][3] instead.
+-- * It will not compress, minify, obfuscate your Lua source code,
+-- or any of the other things [squish][1] can do.
+-- * It doesn't handle the dependencies of C modules, so it is best
+-- used on C modules without dependencies (e.g. LuaSocket, LFS,
+-- etc.).
+--
+-- The `amalg.lua` [source code][6] is available on GitHub, and is
+-- released under the [MIT license][7]. You can view [a nice HTML
+-- version][8] of this file rendered by [Docco][9] on the GitHub
+-- pages.
+--
+-- As already mentioned, there are alternatives to this program: See
+-- [squish][1], [LOOP][2], [soar][3], [luac.lua][4], and
+-- [bundle.lua][5] (and probably some more).
+--
+-- [1]: http://matthewwild.co.uk/projects/squish/home
+-- [2]: http://loop.luaforge.net/release/preload.html
+-- [3]: http://lua-users.org/lists/lua-l/2012-02/msg00609.html
+-- [4]: http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/5.1/luac.lua
+-- [5]: https://github.com/akavel/scissors/blob/master/tools/bundle/bundle.lua
+-- [6]: http://github.com/siffiejoe/lua-amalg
+-- [7]: http://opensource.org/licenses/MIT
+-- [8]: http://siffiejoe.github.io/lua-amalg/
+-- [9]: http://jashkenas.github.io/docco/
+--
+--
+-- ## Getting Started
+--
+-- You can bundle a collection of Lua modules in a single file by
+-- calling the `amalg.lua` script and passing the module names on the
+-- command line:
+--
+-- ./amalg.lua module1 module2
+--
+-- The modules are collected using `package.path`, so they have to be
+-- available there. The resulting merged Lua code will be written to
+-- the standard output stream. You have to actually run the resulting
+-- code to make the embedded Lua modules available for `require`.
+--
+-- You can specify an output file to use instead of the standard
+-- output stream:
+--
+-- ./amalg.lua -o out.lua module1 module2
+--
+-- You can also embed the main script of your application in the
+-- merged Lua code as well. Of course, the embedded Lua modules can be
+-- `require`d from the embedded main script.
+--
+-- ./amalg.lua -o out.lua -s main.lua module1 module2
+--
+-- If you want the original file names and line numbers to appear in
+-- error messages, you have to activate debug mode. This will require
+-- slightly more memory, though.
+--
+-- ./amalg.lua -o out.lua -d -s main.lua module1 module2
+--
+-- To collect all Lua (and C) modules used by a program, you can load
+-- the `amalg.lua` script as a module, and it will intercept calls to
+-- `require` (more specifically the Lua module searchers) and save the
+-- necessary Lua module names in a file `amalg.cache` in the current
+-- directory:
+--
+-- lua -lamalg main.lua
+--
+-- Multiple calls will add to this module cache. But don't access it
+-- from multiple concurrent processes (the cache isn't protected
+-- against race conditions)!
+--
+-- You can use the cache (in addition to all module names given on the
+-- command line) using the `-c` or `-C` flag:
+--
+-- ./amalg.lua -o out.lua -s main.lua -c
+-- ./amalg.lua -o out.lua -s main.lua -C myamalg.cache
+--
+-- However, this will only embed the Lua modules. To also embed the C
+-- modules (both from the cache and from the command line), you have
+-- to specify the `-x` flag:
+--
+-- ./amalg.lua -o out.lua -s main.lua -c -x
+--
+-- This will make the amalgamated script platform- and Lua version
+-- dependent, obviously!
+--
+-- In some cases you may want to ignore automatically listed modules
+-- in the cache without editing the cache file. Use the `-i` option
+-- for that and specify a Lua pattern:
+--
+-- ./amalg.lua -o out.lua -s main.lua -c -i "^luarocks%."
+--
+-- The `-i` option can be used multiple times to specify multiple
+-- patterns.
+--
+-- Usually, the amalgamated modules take precedence over locally
+-- installed (possibly newer) versions of the same modules. If you
+-- want to use local modules when available and only fall back to the
+-- amalgamated code otherwise, you can specify the `-f` flag.
+--
+-- ./amalg.lua -o out.lua -s main.lua -c -f
+--
+-- This installs another searcher/loader function at the end of
+-- `package.searchers` (or `package.loaders` on Lua 5.1) and adds
+-- a new table `package.postload` that serves the same purpose as the
+-- standard `package.preload` table.
+--
+-- To fix a compatibility issue with Lua 5.1's vararg handling,
+-- `amalg.lua` by default adds a local alias to the global `arg` table
+-- to every loaded module. If for some reason you don't want that, use
+-- the `-a` flag (but be aware that in Lua 5.1 with `LUA_COMPAT_VARARG`
+-- defined (the default) your modules can only access the global `arg`
+-- table as `_G.arg`).
+--
+-- ./amalg.lua -o out.lua -a -s main.lua -c
+--
+-- That's it. For further info consult the source.
+--
+--
+-- ## Implementation
+--
+
+-- The name of the script used in warning messages and the name of the
+-- cache file can be configured here by changing these local
+-- variables:
+local prog = "amalg.lua"
+local cache = "amalg.cache"
+
+
+-- Wrong use of the command line may cause warnings to be printed to
+-- the console. This function is for printing those warnings:
+local function warn( ... )
+ io.stderr:write( "WARNING ", prog, ": " )
+ local n = select( '#', ... )
+ for i = 1, n do
+ local v = tostring( (select( i, ... )) )
+ io.stderr:write( v, i == n and '\n' or '\t' )
+ end
+end
+
+
+-- Function for parsing the command line of `amalg.lua` when invoked
+-- as a script. The following flags are supported:
+--
+-- * `-o `: specify output file (default is `stdout`)
+-- * `-s `: specify main script to bundle
+-- * `-c`: add the modules listed in the cache file `amalg.cache`
+-- * `-C `: add the modules listed in the cache file
+-- * `-i `: ignore modules in the cache file matching the
+-- given pattern (can be given multiple times)
+-- * `-d`: enable debug mode (file names and line numbers in error
+-- messages will point to the original location)
+-- * `-a`: do *not* apply the `arg` fix (local alias for the global
+-- `arg` table)
+-- * `-x`: also embed compiled C modules
+-- * `--`: stop parsing command line flags (all remaining arguments
+-- are considered module names)
+--
+-- Other arguments are assumed to be module names. For an inconsistent
+-- command line (e.g. duplicate options) a warning is printed to the
+-- console.
+local function parse_cmdline( ... )
+ local modules, afix, ignores, tname, use_cache, cmods, dbg, script, oname, cname =
+ {}, true, {}, "preload"
+
+ local function set_oname( v )
+ if v then
+ if oname then
+ warn( "Resetting output file `"..oname.."'! Using `"..v.."' now!" )
+ end
+ oname = v
+ else
+ warn( "Missing argument for -o option!" )
+ end
+ end
+
+ local function set_cname( v )
+ if v then
+ if cname then
+ warn( "Resetting cache file `"..cname.."'! Using `"..v.."' now!" )
+ end
+ cname = v
+ else
+ warn( "Missing argument for -C option!" )
+ end
+ end
+
+ local function set_script( v )
+ if v then
+ if script then
+ warn( "Resetting main script `"..script.."'! Using `"..v.."' now!" )
+ end
+ script = v
+ else
+ warn( "Missing argument for -s option!" )
+ end
+ end
+
+ local function add_ignore( v )
+ if v then
+ if not pcall( string.match, "", v ) then
+ warn( "Invalid Lua pattern: `"..v.."'" )
+ else
+ ignores[ #ignores+1 ] = v
+ end
+ else
+ warn( "Missing argument for -i option!" )
+ end
+ end
+
+ local i, n = 1, select( '#', ... )
+ while i <= n do
+ local a = select( i, ... )
+ if a == "--" then
+ for j = i+1, n do
+ modules[ select( j, ... ) ] = true
+ end
+ break
+ elseif a == "-o" then
+ i = i + 1
+ set_oname( i <= n and select( i, ... ) )
+ elseif a == "-s" then
+ i = i + 1
+ set_script( i <= n and select( i, ... ) )
+ elseif a == "-i" then
+ i = i + 1
+ add_ignore( i <= n and select( i, ... ) )
+ elseif a == "-f" then
+ tname = "postload"
+ elseif a == "-c" then
+ use_cache = true
+ elseif a == "-C" then
+ use_cache = true
+ i = i + 1
+ set_cname( i <= n and select( i, ... ) )
+ elseif a == "-x" then
+ cmods = true
+ elseif a == "-d" then
+ dbg = true
+ elseif a == "-a" then
+ afix = false
+ else
+ local prefix = a:sub( 1, 2 )
+ if prefix == "-o" then
+ set_oname( a:sub( 3 ) )
+ elseif prefix == "-s" then
+ set_script( a:sub( 3 ) )
+ elseif prefix == "-i" then
+ add_ignore( a:sub( 3 ) )
+ elseif a:sub( 1, 1 ) == "-" then
+ warn( "Unknown command line flag: "..a )
+ else
+ modules[ a ] = true
+ end
+ end
+ i = i + 1
+ end
+ return oname, script, dbg, afix, use_cache, tname, ignores, cmods, modules, cname
+end
+
+
+-- The approach for embedding precompiled Lua files is different from
+-- the normal way of pasting the source code, so this function detects
+-- whether a file is a binary file (Lua bytecode starts with the `ESC`
+-- character):
+local function is_bytecode( path )
+ local f, res = io.open( path, "rb" ), false
+ if f then
+ res = f:read( 1 ) == "\027"
+ f:close()
+ end
+ return res
+end
+
+
+-- Read the whole contents of a file into memory without any
+-- processing.
+local function readfile( path, is_bin )
+ local f = assert( io.open( path, is_bin and "rb" or "r" ) )
+ local s = assert( f:read( "*a" ) )
+ f:close()
+ return s
+end
+
+
+-- Lua files to be embedded into the resulting amalgamation are read
+-- into memory in a single go, because under some circumstances (e.g.
+-- binary chunks, shebang lines, `-d` command line flag) some
+-- preprocessing/escaping is necessary. This function reads a whole
+-- Lua file and returns the contents as a Lua string.
+local function readluafile( path )
+ local is_bin = is_bytecode( path )
+ local s = readfile( path, is_bin )
+ local shebang
+ if not is_bin then
+ -- Shebang lines are only supported by Lua at the very beginning
+ -- of a source file, so they have to be removed before the source
+ -- code can be embedded in the output.
+ shebang = s:match( "^(#![^\n]*)" )
+ s = s:gsub( "^#[^\n]*", "" )
+ end
+ return s, is_bin, shebang
+end
+
+
+-- Lua 5.1's `string.format("%q")` doesn't convert all control
+-- characters to decimal escape sequences like the newer Lua versions
+-- do. This might cause problems on some platforms (i.e. Windows) when
+-- loading a Lua script (opened in text mode) that contains binary
+-- code.
+local function qformat( code )
+ local s = ("%q"):format( code )
+ return (s:gsub( "(%c)(%d?)", function( c, d )
+ if c ~= "\n" then
+ return (d~="" and "\\%03d" or "\\%d"):format( c:byte() )..d
+ end
+ end ))
+end
+
+
+-- When the `-c` command line flag is given, the contents of the cache
+-- file `amalg.cache` are used to specify the modules to embed. This
+-- function is used to load the cache file. ist optional:
+local function readcache( filename )
+ local chunk = loadfile( filename or cache, "t", {} )
+ if chunk then
+ if setfenv then setfenv( chunk, {} ) end
+ local result = chunk()
+ if type( result ) == "table" then
+ return result
+ end
+ end
+end
+
+
+-- When loaded as a module, `amalg.lua` collects Lua modules and C
+-- modules that are `require`d and updates the cache file
+-- `amalg.cache`. This function saves the updated cache contents to
+-- the file:
+local function writecache( c )
+ local f = assert( io.open( cache, "w" ) )
+ f:write( "return {\n" )
+ for k,v in pairs( c ) do
+ if type( k ) == "string" and type( v ) == "string" then
+ f:write( " [ ", qformat( k ), " ] = ", qformat( v ), ",\n" )
+ end
+ end
+ f:write( "}\n" )
+ f:close()
+end
+
+
+-- The standard Lua function `package.searchpath` available in Lua 5.2
+-- and up is used to locate the source files for Lua modules and
+-- library files for C modules. For Lua 5.1 a backport is provided.
+local searchpath = package.searchpath
+if not searchpath then
+ local delim = package.config:match( "^(.-)\n" ):gsub( "%%", "%%%%" )
+
+ function searchpath( name, path )
+ local pname = name:gsub( "%.", delim ):gsub( "%%", "%%%%" )
+ local msg = {}
+ for subpath in path:gmatch( "[^;]+" ) do
+ local fpath = subpath:gsub( "%?", pname )
+ local f = io.open( fpath, "r" )
+ if f then
+ f:close()
+ return fpath
+ end
+ msg[ #msg+1 ] = "\n\tno file '"..fpath.."'"
+ end
+ return nil, table.concat( msg )
+ end
+end
+
+
+-- This is the main function for the use case where `amalg.lua` is run
+-- as a script. It parses the command line, creates the output files,
+-- collects the module and script sources, and writes the amalgamated
+-- source.
+local function amalgamate( ... )
+ local oname, script, dbg, afix, use_cache, tname, ignores, cmods, modules, cname =
+ parse_cmdline( ... )
+ local errors = {}
+
+ -- When instructed to on the command line, the cache file is loaded,
+ -- and the modules are added to the ones listed on the command line
+ -- unless they are ignored via the `-i` command line option.
+ if use_cache then
+ local c = readcache( cname )
+ for k,v in pairs( c or {} ) do
+ local addmodule = true
+ for _,p in ipairs( ignores ) do
+ if k:match( p ) then
+ addmodule = false
+ break
+ end
+ end
+ if addmodule then
+ modules[ k ] = v
+ end
+ end
+ end
+
+ local out = io.stdout
+ if oname then
+ out = assert( io.open( oname, "w" ) )
+ end
+
+ -- If a main script is to be embedded, this includes the same
+ -- shebang line that was used in the main script, so that the
+ -- resulting amalgamation can be run without explicitly
+ -- specifying the interpreter on unixoid systems (if a shebang
+ -- line was specified in the first place, that is).
+ local script_bytes, script_binary, shebang
+ if script then
+ script_bytes, script_binary, shebang = readluafile( script )
+ if shebang then
+ out:write( shebang, "\n\n" )
+ end
+ out:write( "do\n\n" )
+ end
+
+ -- If fallback loading is requested, the module loaders of the
+ -- amalgamated module are registered in table `package.postload`,
+ -- and an extra searcher function is added at the end of
+ -- `package.searchers`.
+ if tname == "postload" then
+ out:write([=[
+do
+ local assert = assert
+ local type = assert( type )
+ local searchers = package.searchers or package.loaders
+ local postload = {}
+ package.postload = postload
+ searchers[ #searchers+1 ] = function( mod )
+ assert( type( mod ) == "string", "module name must be a string" )
+ local loader = postload[ mod ]
+ if loader == nil then
+ return "\n\tno field package.postload['"..mod.."']"
+ else
+ return loader
+ end
+ end
+end
+
+]=] )
+ end
+
+ -- Sort modules alphabetically. Modules will be embedded in
+ -- alphabetical order. This ensures deterministic output.
+ local module_names = {}
+ for m in pairs( modules ) do
+ module_names[ #module_names+1 ] = m
+ end
+ table.sort( module_names )
+
+ -- Every module given on the command line and/or in the cache file
+ -- is processed.
+ for _,m in ipairs( module_names ) do
+ local t = modules[ m ]
+ -- Only Lua modules are handled for now, so modules that are
+ -- definitely C modules are skipped and handled later.
+ if t ~= "C" then
+ local path, msg = searchpath( m, package.path )
+ if not path and (t == "L" or not cmods) then
+ -- The module is supposed to be a Lua module, but it cannot
+ -- be found, so an error is raised.
+ error( "module `"..m.."' not found:"..msg )
+ elseif not path then
+ -- Module possibly is a C module, so it is tried again later.
+ -- But the current error message is saved in case the given
+ -- name isn't a C module either.
+ modules[ m ], errors[ m ] = "C", msg
+ else
+ local bytes, is_bin = readluafile( path )
+ if is_bin or dbg then
+ -- Precompiled Lua modules are loaded via the standard Lua
+ -- function `load` (or `loadstring` in Lua 5.1). Since this
+ -- preserves file name and line number information, this
+ -- approach is used for all files if the debug mode is active
+ -- (`-d` command line option).
+ out:write( "package.", tname, "[ ", qformat( m ),
+ " ] = assert( (loadstring or load)(\n",
+ qformat( bytes ), "\n, '@'..",
+ qformat( path ), " ) )\n\n" )
+ else
+ -- Under normal circumstances Lua files are pasted into a
+ -- new anonymous vararg function, which then is put into
+ -- `package.preload` so that `require` can find it. Each
+ -- function gets its own `_ENV` upvalue (on Lua 5.2+), and
+ -- special care is taken that `_ENV` always is the first
+ -- upvalue (important for the `module` function on Lua 5.2).
+ -- Lua 5.1 compiled with `LUA_COMPAT_VARARG` (the default) will
+ -- create a local `arg` variable to emulate the vararg handling
+ -- of Lua 5.0. This might interfere with Lua modules that access
+ -- command line arguments via the `arg` global. As a workaround
+ -- `amalg.lua` adds a local alias to the global `arg` table
+ -- unless the `-a` command line flag is specified.
+ out:write( "do\nlocal _ENV = _ENV\n",
+ "package.", tname, "[ ", qformat( m ),
+ " ] = function( ... ) ",
+ afix and "local arg = _G.arg;\n" or "_ENV = _ENV;\n",
+ bytes, "\nend\nend\n\n" )
+ end
+ end
+ end
+ end
+
+ -- If the `-x` command line flag is active, C modules are embedded
+ -- as strings, and written out to temporary files on demand by the
+ -- amalgamated code.
+ if cmods then
+ local nfuncs = {}
+ -- To make the loading of C modules more robust, the necessary
+ -- global functions are saved in upvalues (because user-supplied
+ -- code might be run before a C module is loaded). The upvalues
+ -- are local to a `do ... end` block, so they aren't visible in
+ -- the main script code.
+ --
+ -- On Windows the result of `os.tmpname()` is not an absolute
+ -- path by default. If that's the case the value of the `TMP`
+ -- environment variable is prepended to make it absolute.
+ local prefix = [=[
+local assert = assert
+local newproxy = newproxy
+local getmetatable = assert( getmetatable )
+local setmetatable = assert( setmetatable )
+local os_tmpname = assert( os.tmpname )
+local os_getenv = assert( os.getenv )
+local os_remove = assert( os.remove )
+local io_open = assert( io.open )
+local string_match = assert( string.match )
+local string_sub = assert( string.sub )
+local package_loadlib = assert( package.loadlib )
+
+local dirsep = package.config:match( "^([^\n]+)" )
+local tmpdir
+local function newdllname()
+ local tmpname = assert( os_tmpname() )
+ if dirsep == "\\" then
+ if not string_match( tmpname, "[\\/][^\\/]+[\\/]" ) then
+ tmpdir = tmpdir or assert( os_getenv( "TMP" ) or
+ os_getenv( "TEMP" ),
+ "could not detect temp directory" )
+ local first = string_sub( tmpname, 1, 1 )
+ local hassep = first == "\\" or first == "/"
+ tmpname = tmpdir..((hassep) and "" or "\\")..tmpname
+ end
+ end
+ return tmpname
+end
+local dllnames = {}
+
+]=]
+ for _,m in ipairs( module_names ) do
+ local t = modules[ m ]
+ if t == "C" then
+ -- Try a search strategy similar to the standard C module
+ -- searcher first and then the all-in-one strategy to locate
+ -- the library files for the C modules to embed.
+ local path, msg = searchpath( m, package.cpath )
+ if not path then
+ errors[ m ] = (errors[ m ] or "") .. msg
+ path, msg = searchpath( m:gsub( "%..*$", "" ), package.cpath )
+ if not path then
+ error( "module `"..m.."' not found:"..errors[ m ]..msg )
+ end
+ end
+ local qpath = qformat( path )
+ -- Build the symbol(s) to look for in the dynamic library.
+ -- There may be multiple candidates because of optional
+ -- version information in the module names and the different
+ -- approaches of the different Lua versions in handling that.
+ local openf = m:gsub( "%.", "_" )
+ local openf1, openf2 = openf:match( "^([^%-]*)%-(.*)$" )
+ -- The amalgamation of C modules is split into two parts:
+ -- One part generates a temporary file name for the C library
+ -- and writes the binary code stored in the amalgamation to
+ -- that file, while the second loads the resulting dynamic
+ -- library using `package.loadlib`. The split is necessary
+ -- because multiple modules could be loaded from the same
+ -- library, and the amalgamated code has to simulate that.
+ -- Shared dynamic libraries are embedded only once.
+ --
+ -- The temporary dynamic library files may or may not be
+ -- cleaned up when the amalgamated code exits (this probably
+ -- works on POSIX machines (all Lua versions) and on Windows
+ -- with Lua 5.1). The reason is that starting with version 5.2
+ -- Lua ensures that libraries aren't unloaded before normal
+ -- user-supplied `__gc` metamethods have run to avoid a case
+ -- where such a metamethod would call an unloaded C function.
+ -- As a consequence the amalgamated code tries to remove the
+ -- temporary library files *before* they are actually
+ -- unloaded.
+ if not nfuncs[ path ] then
+ local code = readfile( path, true )
+ nfuncs[ path ] = true
+ local qcode = qformat( code )
+ out:write( prefix, "dllnames[ ", qpath, [=[ ] = function()
+ local dll = newdllname()
+ local f = assert( io_open( dll, "wb" ) )
+ f:write( ]=], qcode, [=[ )
+ f:close()
+ local sentinel = newproxy and newproxy( true )
+ or setmetatable( {}, { __gc = true } )
+ getmetatable( sentinel ).__gc = function() os_remove( dll ) end
+ dllnames[ ]=], qpath, [=[ ] = function()
+ local _ = sentinel
+ return dll
+ end
+ return dll
+end
+
+]=] )
+ prefix = ""
+ end -- shared libary not embedded already
+ -- Add a function to `package.preload` to load the temporary
+ -- DLL or shared object file. This function tries to mimic the
+ -- behavior of Lua 5.3 which is to strip version information
+ -- from the module name at the end first, and then at the
+ -- beginning if that failed.
+ local qm = qformat( m )
+ out:write( "package.", tname, "[ ", qm, " ] = function()\n",
+ " local dll = dllnames[ ", qpath, " ]()\n" )
+ if openf1 then
+ out:write( " local loader = package_loadlib( dll, ",
+ qformat( "luaopen_"..openf1 ), " )\n",
+ " if not loader then\n",
+ " loader = assert( package_loadlib( dll, ",
+ qformat( "luaopen_"..openf2 ),
+ " ) )\n end\n" )
+ else
+ out:write( " local loader = assert( package_loadlib( dll, ",
+ qformat( "luaopen_"..openf ), " ) )\n" )
+ end
+ out:write( " return loader( ", qm, ", dll )\nend\n\n" )
+ end -- is a C module
+ end -- for all given module names
+ end -- if cmods
+
+ -- If a main script is specified on the command line (`-s` flag),
+ -- embed it now that all dependent modules are available to
+ -- `require`.
+ if script then
+ out:write( "end\n\n" )
+ if script_binary or dbg then
+ out:write( "assert( (loadstring or load)(\n",
+ qformat( script_bytes ), "\n, '@'..",
+ qformat( script ), " ) )( ... )\n\n" )
+ else
+ out:write( script_bytes )
+ end
+ end
+
+ if oname then
+ out:close()
+ end
+end
+
+
+-- If `amalg.lua` is loaded as a module, it intercepts `require` calls
+-- (more specifically calls to the searcher functions) to collect all
+-- `require`d module names and store them in the cache. The cache file
+-- `amalg.cache` is updated when the program terminates.
+local function collect()
+ local searchers = package.searchers or package.loaders
+ -- When the searchers table has been modified, it is unknown which
+ -- elements in the table to replace, so `amalg.lua` bails out with
+ -- an error. The `luarocks.loader` module which inserts itself at
+ -- position 1 in the `package.searchers` table is explicitly
+ -- supported, though!
+ local off = 0
+ if package.loaded[ "luarocks.loader" ] then off = 1 end
+ assert( #searchers == 4+off, "package.searchers has been modified" )
+ local c = readcache() or {}
+ -- The updated cache is written to disk when the following value is
+ -- garbage collected, which should happen at `lua_close()`.
+ local sentinel = newproxy and newproxy( true )
+ or setmetatable( {}, { __gc = true } )
+ getmetatable( sentinel ).__gc = function() writecache( c ) end
+ local lua_searcher = searchers[ 2+off ]
+ local c_searcher = searchers[ 3+off ]
+ local aio_searcher = searchers[ 4+off ] -- all in one searcher
+
+ local function rv_handler( tag, mname, ... )
+ if type( (...) ) == "function" then
+ c[ mname ] = tag
+ end
+ return ...
+ end
+
+ -- The replacement searchers just forward to the original versions,
+ -- but also update the cache if the search was successful.
+ searchers[ 2+off ] = function( ... )
+ local _ = sentinel -- make sure that sentinel is an upvalue
+ return rv_handler( "L", ..., lua_searcher( ... ) )
+ end
+ searchers[ 3+off ] = function( ... )
+ local _ = sentinel -- make sure that sentinel is an upvalue
+ return rv_handler( "C", ..., c_searcher( ... ) )
+ end
+ searchers[ 4+off ] = function( ... )
+ local _ = sentinel -- make sure that sentinel is an upvalue
+ return rv_handler( "C", ..., aio_searcher( ... ) )
+ end
+
+ -- Since calling `os.exit` might skip the `lua_close()` call, the
+ -- `os.exit` function is monkey-patched to also save the updated
+ -- cache to the cache file on disk.
+ if type( os ) == "table" and type( os.exit ) == "function" then
+ local os_exit = os.exit
+ function os.exit( ... )
+ writecache( c )
+ return os_exit( ... )
+ end
+ end
+end
+
+
+-- To determine whether `amalg.lua` is run as a script or loaded as a
+-- module it uses the debug module to walk the call stack looking for
+-- a `require` call. If such a call is found, `amalg.lua` has been
+-- `require`d as a module.
+local function is_script()
+ local i = 3
+ local info = debug.getinfo( i, "f" )
+ while info do
+ if info.func == require then
+ return false
+ end
+ i = i + 1
+ info = debug.getinfo( i, "f" )
+ end
+ return true
+end
+
+
+-- This checks whether `amalg.lua` has been called as a script or
+-- loaded as a module and acts accordingly, by calling the
+-- corresponding main function:
+if is_script() then
+ amalgamate( ... )
+else
+ collect()
+end
\ No newline at end of file
diff --git a/tools/unittest.sh b/tools/unittest.sh
new file mode 100755
index 0000000..a3ddd20
--- /dev/null
+++ b/tools/unittest.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# This script finds and runs Lua unit tests.
+
+readonly script_dir=$(dirname "$(readlink -f "$0")")
+if [[ -v $1 ]]
+then
+ readonly base_dir="$1"
+else
+ readonly base_dir=$(readlink -f "$script_dir/..")
+fi
+
+readonly src_module_path="$base_dir/src/main/lua"
+readonly test_module_path="$base_dir/src/test/lua"
+readonly target_dir="$base_dir/target"
+readonly reports_dir="$target_dir/luaunit_reports"
+
+mkdir -p "$reports_dir"
+
+cd $test_module_path
+readonly tests="$(find . -name '*.lua')"
+
+for testcase in $tests
+do
+ testname=$(echo "$testcase" | sed -e s'/.\///' -e s'/\//./g' -e s'/.lua$//')
+ LUA_PATH="$src_module_path/?.lua;$(luarocks path --lr-path)" lua "$testcase" -o junit -n "$reports_dir/$testname"
+done
\ No newline at end of file