From fb49dcfcb8e6cbba61032d95371510519ee186e3 Mon Sep 17 00:00:00 2001 From: Kory Draughn Date: Mon, 1 Apr 2024 15:51:17 -0400 Subject: [PATCH] [#7570] Complete port of GenQuery2 parser. --- lib/api/include/irods/genquery2.h | 13 +- plugins/database/src/db_plugin.cpp | 4 +- scripts/irods/test/test_dynamic_peps.py | 170 ++++++++++++++++++ scripts/irods/test/test_federation.py | 16 +- .../test/test_genquery2_microservices.py | 36 ++-- scripts/irods/test/test_iquery.py | 41 ++++- server/api/include/irods/rs_genquery2.hpp | 8 +- server/api/src/rs_genquery2.cpp | 12 +- server/genquery2/dsl/parser.y | 2 +- .../irods/private/genquery2_ast_types.hpp | 8 +- .../irods/private/genquery2_driver.hpp | 4 +- .../irods/private/genquery2_scanner.hpp | 6 +- .../include/irods/private/genquery2_sql.hpp | 4 +- .../genquery2_table_column_mappings.hpp | 21 ++- server/genquery2/src/genquery2_sql.cpp | 85 +++++---- .../include/irods/icatHighLevelRoutines.hpp | 8 +- server/re/include/irods/msi_genquery2.hpp | 104 ++++++++++- server/re/src/irods_re_serialization.cpp | 92 +++++++++- server/re/src/msi_genquery2.cpp | 118 ++---------- 19 files changed, 530 insertions(+), 222 deletions(-) rename server/genquery2/include/irods/{ => private}/genquery2_table_column_mappings.hpp (95%) diff --git a/lib/api/include/irods/genquery2.h b/lib/api/include/irods/genquery2.h index 4253285f92..b5894bd611 100644 --- a/lib/api/include/irods/genquery2.h +++ b/lib/api/include/irods/genquery2.h @@ -10,11 +10,11 @@ struct RcComm; /// \warning This data structure is part of an experimental API and may change in the future. /// /// \since 4.3.2 -typedef struct Genquery2Input // NOLINT(modernize-use-using) +typedef struct Genquery2Input { /// The GenQuery2 query string to execute. /// - /// This member variable is allowed to be set to NULL when + /// This member variable is allowed to be set to NULL. /// /// \since 4.3.2 char* query_string; @@ -46,7 +46,6 @@ typedef struct Genquery2Input // NOLINT(modernize-use-using) int column_mappings; } genquery2Inp_t; -// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) #define Genquery2Input_PI "str *query_string; str *zone; int sql_only; int column_mappings;" #ifdef __cplusplus @@ -75,7 +74,7 @@ extern "C" { /// /// Limitations: /// - Groups are not yet fully supported -/// - Cannot resolve tickets to data objects and collections using a single query +/// - Tickets are not yet supported. /// - Integer values must be treated as strings, except when used for OFFSET, LIMIT, FETCH FIRST \a N ROWS ONLY /// /// When the query does not include the FETCH FIRST \a N ROWS ONLY or LIMIT clause, the API will clamp the @@ -87,9 +86,9 @@ extern "C" { /// The column mappings between GenQuery2 and the catalog can be obtained via the API. See the Genquery2Input /// structure for details. /// -/// \param[in] _comm A pointer to a RcComm. -/// \param[in] _input A pointer to a Genquery2Input. -/// \param[in,out] _output \parblock A pointer that will hold the results of the operation. +/// \param[in] _comm A pointer to a RcComm. +/// \param[in] _input A pointer to a Genquery2Input. +/// \param[out] _output \parblock A pointer that will hold the results of the operation. /// On success, the pointer will either hold a JSON string or a string representing the SQL derived from /// the GenQuery2 query string. See Genquery2Input::sql_only for details. /// diff --git a/plugins/database/src/db_plugin.cpp b/plugins/database/src/db_plugin.cpp index 5008d3495f..f999490f25 100644 --- a/plugins/database/src/db_plugin.cpp +++ b/plugins/database/src/db_plugin.cpp @@ -15833,7 +15833,7 @@ auto db_execute_genquery2_sql(irods::plugin_context& _ctx, if (!_sql || !_values || !_output) { log_db::error("{}: Received one or more null pointers.", __func__); - return ERROR(SYS_INVALID_INPUT_PARAM, "Received one or more null pointers."); + return ERROR(SYS_INTERNAL_NULL_INPUT_ERR, "Received one or more null pointers."); } *_output = nullptr; @@ -15871,7 +15871,7 @@ auto db_execute_genquery2_sql(irods::plugin_context& _ctx, } catch (const irods::exception& e) { log_db::error("{}: {}", __func__, e.client_display_what()); - return ERROR(SYS_LIBRARY_ERROR, e.what()); + return ERROR(e.code(), e.what()); } catch (const std::exception& e) { log_db::error("{}: {}", __func__, e.what()); diff --git a/scripts/irods/test/test_dynamic_peps.py b/scripts/irods/test/test_dynamic_peps.py index 841b700a7e..ff4210ae17 100644 --- a/scripts/irods/test/test_dynamic_peps.py +++ b/scripts/irods/test/test_dynamic_peps.py @@ -13,6 +13,7 @@ from .. import paths from .. import test from ..configuration import IrodsConfig +from ..controller import IrodsController from ..core_file import temporary_core_file from textwrap import dedent @@ -322,3 +323,172 @@ def test_pep_database_data_object_finalize__issue_6385(self): self.user.run_icommand(['irm', '-f', logical_path]) lib.remove_resource(self.admin, other_resource) + + @unittest.skipIf(plugin_name != 'irods_rule_engine_plugin-irods_rule_language' or test.settings.RUN_IN_TOPOLOGY, "Requires NREP and single node zone.") + def test_if_const_vector_of_strings_is_exposed__issue_7570(self): + with temporary_core_file() as core: + prefix = 'test_if_const_vector_of_strings_is_exposed__issue_7570' + attr_name_1 = f'{prefix}_iquery_input_value' + attr_value_1 = f'{prefix}_vector_of_strings' + attr_name_2 = f'{prefix}_vector_of_strings_size' + + # Add a rule to core.re which when triggered will add the vector size and elements + # of "*values" to the session collection as AVUs. + core.add_rule(dedent(''' + pep_database_execute_genquery2_sql_pre(*inst, *ctx, *out, *sql, *values, *output) {{ + msiModAVUMetadata('-C', '{self.user.session_collection}', 'add', '{attr_name_1}', *values."0", ''); + msiModAVUMetadata('-C', '{self.user.session_collection}', 'add', '{attr_name_2}', *values."size", ''); + }} + '''.format(**locals()))) + + try: + # Trigger PEP, ignore output. + self.user.assert_icommand(['iquery', f"select COLL_NAME where COLL_NAME = '{attr_value_1}'"], 'STDOUT') + + # Show the session collection has an AVU attached to it. + # This is only possible if vectors of strings are serializable. + self.user.assert_icommand(['imeta', 'ls', '-C', self.user.session_collection], 'STDOUT', [ + f'attribute: {attr_name_1}\nvalue: {attr_value_1}\n', + # The value is 2 due to the parser adding additional bind variables. + # For this specific case, a second bind variable for the username is included. + f'attribute: {attr_name_2}\nvalue: 2\n' + ]) + + finally: + # This isn't required, but we do it here to keep the database metadata + # tables small, for performance reasons. + self.user.run_icommand(['imeta', 'rm', '-C', self.user.session_collection, attr_name_1, attr_value_1]) + self.user.run_icommand(['imeta', 'rm', '-C', self.user.session_collection, attr_name_2, '2']) + + @unittest.skipIf(plugin_name != 'irods_rule_engine_plugin-irods_rule_language' or test.settings.RUN_IN_TOPOLOGY, 'Requires NREP and single node zone.') + def test_if_vector_of_strings_is_exposed__issue_7570(self): + config = IrodsConfig() + + with lib.file_backed_up(config.server_config_path): + try: + # Lower the delay server's sleep time so that rule are executed quicker. + config.server_config['advanced_settings']['delay_server_sleep_time_in_seconds'] = 1 + lib.update_json_file_from_dict(config.server_config_path, config.server_config) + + # Restart so the new server configuration takes effect. + IrodsController().restart(test_mode=True) + + with temporary_core_file() as core: + prefix = 'test_if_vector_of_strings_is_exposed__issue_7570' + attr_name_1 = f'{prefix}_delay_rule_info_element_value' + attr_name_2 = f'{prefix}_vector_of_strings_size' + + # Add a rule to core.re which when triggered will add the vector size and elements + # of "*delay_rule_info" to "self.admin's" session collection as AVUs. + core.add_rule(dedent(''' + pep_database_get_delay_rule_info_post(*inst, *ctx, *out, *rule_id, *delay_rule_info) {{ + *trimmed_value = trimr(triml(*delay_rule_info."0", ' '), ' '); # Remove leading/trailing whitespace. + msiModAVUMetadata('-C', '{self.admin.session_collection}', 'add', '{attr_name_1}', *trimmed_value, ''); + msiModAVUMetadata('-C', '{self.admin.session_collection}', 'add', '{attr_name_2}', *delay_rule_info."size", ''); + }} + '''.format(**locals()))) + + # Give the service account user permission to add metadata to "self.admin's" session collection. + # Remember, "self.admin" represents a completely different rodsadmin AND the PEP that was added + # to core.re is only triggered by the service account user. + self.admin.assert_icommand( + ['ichmod', 'modify_object', config.client_environment['irods_user_name'], self.admin.session_collection]) + + # Placed here for clean-up purposes. + rep_name = self.plugin_name + '-instance' + delay_rule_body = 'writeLine("serverLog", "test_if_vector_of_strings_is_exposed__issue_7570");' + rule = f'delay("{rep_name}") {{ {delay_rule_body} }}' + + try: + # Schedule a delay rule to trigger the database PEP. + self.admin.assert_icommand(['irule', '-r', rep_name, rule, 'null', 'null']) + + # Show the session collection has an AVU attached to it. + # This is only possible if vectors of strings are serializable. + + def has_avu(): + out, _, _ = self.admin.run_icommand(['imeta', 'ls', '-C', self.admin.session_collection]) + print(out) # For debugging purposes. + return all(avu in out for avu in [ + f'attribute: {attr_name_1}\nvalue: {delay_rule_body}\n', + f'attribute: {attr_name_2}\nvalue: 12\n' + ]) + + lib.delayAssert(lambda: has_avu()) + + finally: + # This isn't required, but we do it here to keep the database metadata + # tables small, for performance reasons. + self.admin.run_icommand(['imeta', 'rm', '-C', self.admin.session_collection, attr_name_1, delay_rule_body]) + self.admin.run_icommand(['imeta', 'rm', '-C', self.admin.session_collection, attr_name_2, '12']) + + finally: + # Restart so the server's original configuration takes effect. + IrodsController().restart(test_mode=True) + + @unittest.skipIf(plugin_name != 'irods_rule_engine_plugin-irods_rule_language' or test.settings.RUN_IN_TOPOLOGY, "Requires NREP and single node zone.") + def test_if_Genquery2Input_is_exposed__issue_7570(self): + with temporary_core_file() as core: + avu_prefix = 'test_if_Genquery2Input_is_exposed__issue_7570' + + # Add a rule to core.re which when triggered will add AVUs to the user's + # session collection. + core.add_rule(dedent(''' + pep_api_genquery2_pre(*inst, *comm, *input, *output) {{ + *v = *input.query_string; + msiModAVUMetadata('-C', '{self.user.session_collection}', 'add', '{avu_prefix}_0', *v, ''); + + *v = *input.zone; + if (strlen(*v) > 0) {{ + msiModAVUMetadata('-C', '{self.user.session_collection}', 'add', '{avu_prefix}_1', *v, ''); + }} + else {{ + msiModAVUMetadata('-C', '{self.user.session_collection}', 'add', '{avu_prefix}_1', 'unspecified', ''); + }} + + *v = *input.sql_only; + msiModAVUMetadata('-C', '{self.user.session_collection}', 'add', '{avu_prefix}_2', *v, ''); + + *v = *input.column_mappings; + msiModAVUMetadata('-C', '{self.user.session_collection}', 'add', '{avu_prefix}_3', *v, ''); + }} + '''.format(**locals()))) + + try: + # Trigger PEP, ignore output. + query_string = f"select COLL_NAME where COLL_NAME = '{self.user.session_collection}'" + self.user.assert_icommand(['iquery', query_string], 'STDOUT') + + # Show the session collection has the expected AVUs attached to it. + self.user.assert_icommand(['imeta', 'ls', '-C', self.user.session_collection], 'STDOUT', [ + f'attribute: {avu_prefix}_0\nvalue: {query_string}\n', + f'attribute: {avu_prefix}_1\nvalue: unspecified\n', + f'attribute: {avu_prefix}_2\nvalue: 0\n', + f'attribute: {avu_prefix}_3\nvalue: 0\n', + ]) + + # Remove the AVUs to avoid duplicate AVU errors. These will cause the test to fail. + self.user.assert_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_0', query_string]) + self.user.assert_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_1', 'unspecified']) + self.user.assert_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_2', '0']) + self.user.assert_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_3', '0']) + + # Now, with the zone explicitly passed. + self.user.assert_icommand(['iquery', '-z', self.user.zone_name, query_string], 'STDOUT') + + # Show the session collection has the expected AVUs attached to it. + self.user.assert_icommand(['imeta', 'ls', '-C', self.user.session_collection], 'STDOUT', [ + f'attribute: {avu_prefix}_0\nvalue: {query_string}\n', + f'attribute: {avu_prefix}_1\nvalue: {self.user.zone_name}\n', + f'attribute: {avu_prefix}_2\nvalue: 0\n', + f'attribute: {avu_prefix}_3\nvalue: 0\n', + ]) + + finally: + # This isn't required, but we do it here to keep the database metadata + # tables small, for performance reasons. + self.user.run_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_0', query_string]) + self.user.run_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_1', 'unspecified']) + self.user.run_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_1', self.user.zone_name]) + self.user.run_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_2', '0']) + self.user.run_icommand(['imeta', 'rm', '-C', self.user.session_collection, f'{avu_prefix}_3', '0']) diff --git a/scripts/irods/test/test_federation.py b/scripts/irods/test/test_federation.py index 9f62dc241c..6a8f327e7e 100644 --- a/scripts/irods/test/test_federation.py +++ b/scripts/irods/test/test_federation.py @@ -19,6 +19,18 @@ SessionsMixin = session.make_sessions_mixin( test.settings.FEDERATION.RODSADMIN_NAME_PASSWORD_LIST, test.settings.FEDERATION.RODSUSER_NAME_PASSWORD_LIST) +# This test script expects tempZone to contain the following users: +# +# - rods#tempZone +# - zonehopper#tempZone +# - zonehopper#otherZone +# +# otherZone must contain the following user: +# +# - rods#otherZone +# +# The test script must be launched from a server in otherZone. That means tempZone +# is identified as the remote federated zone. class Test_ICommands(SessionsMixin, unittest.TestCase): @@ -2386,8 +2398,8 @@ class Test_GenQuery2_IQuery(SessionsMixin, unittest.TestCase): # This test suite expects tempZone to contain the following users: # # - rods#tempZone - # - zonehopper#tempZone (created by the irods_testing_environment) - # - zonehopper#otherZone (created by the irods_testing_environment) + # - zonehopper#tempZone + # - zonehopper#otherZone # # otherZone must contain the following users: # diff --git a/scripts/irods/test/test_genquery2_microservices.py b/scripts/irods/test/test_genquery2_microservices.py index 4f33a924e1..ec8374c490 100644 --- a/scripts/irods/test/test_genquery2_microservices.py +++ b/scripts/irods/test/test_genquery2_microservices.py @@ -20,12 +20,11 @@ def setUp(self): def tearDown(self): super(Test_GenQuery2_Microservices, self).tearDown() - @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-irods_rule_language', 'Designed for the NREP') - def test_microservices_can_be_used_in_the_native_rule_engine_plugin__issue_7570(self): - rule_file = f'{self.user.local_session_dir}/test_microservices_can_be_used_in_the_native_rule_engine_plugin__issue_7570.nrep.r' - - with open(rule_file, 'w') as f: - f.write(textwrap.dedent(f''' + @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-irods_rule_language', '#7638: Need access to error code in PREP for msi_genquery2_next_row()') + def test_genquery2_microservices_can_be_used_within_policy__issue_7570(self): + rule_map = { + # NREP rule + 'irods_rule_engine_plugin-irods_rule_language': textwrap.dedent(f''' test_issue_7570_nrep {{ *END_OF_RESULTSET = -408000; @@ -51,24 +50,16 @@ def test_microservices_can_be_used_in_the_native_rule_engine_plugin__issue_7570( INPUT *handle="", *coll_name="" OUTPUT ruleExecOut - ''')) - - rep_name = self.plugin_name + '-instance' - self.user.assert_icommand(['irule', '-r', rep_name, '-F', rule_file], 'STDOUT', [self.user.session_collection]) - - @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-python', 'Designed for the PREP') - def test_microservices_can_be_used_in_the_python_rule_engine_plugin(self): - rule_file = f'{self.user.local_session_dir}/test_microservices_can_be_used_in_the_python_rule_engine_plugin.prep.r' - - with open(rule_file, 'w') as f: - f.write(textwrap.dedent(f''' + '''), + # PREP rule + 'irods_rule_engine_plugin-python': textwrap.dedent(f''' test_issue_7570_prep(rule_args, callback, rei) {{ result = callback.msi_genquery2_execute('', "select COLL_NAME where COLL_NAME = '{self.user.session_collection}'"); handle = result['arguments'][0] while True: try: - msi_genquery2_next_row(*handle) + callback.msi_genquery2_next_row(*handle) except: break @@ -82,13 +73,18 @@ def test_microservices_can_be_used_in_the_python_rule_engine_plugin(self): INPUT *handle=%*coll_name= OUTPUT *ruleExecOut - ''')) + ''') + } + + rule_file = f'{self.user.local_session_dir}/test_genquery2_microservices_can_be_used_within_policy__issue_7570.{self.plugin_name}.r' + with open(rule_file, 'w') as f: + f.write(rule_map[self.plugin_name]) rep_name = self.plugin_name + '-instance' self.user.assert_icommand(['irule', '-r', rep_name, '-F', rule_file], 'STDOUT', [self.user.session_collection]) @unittest.skipUnless(plugin_name == 'irods_rule_engine_plugin-irods_rule_language', 'Designed for the NREP') - def test_microservices_support_interleaved_execution__issue_7570(self): + def test_genquery2_microservices_support_interleaved_execution__issue_7570(self): # This test is about proving GenQuery2 handles are tracked correctly. # # The original GenQuery2 implementation used a std::vector to track handles. This diff --git a/scripts/irods/test/test_iquery.py b/scripts/irods/test/test_iquery.py index e3daf35eb5..2234db6851 100644 --- a/scripts/irods/test/test_iquery.py +++ b/scripts/irods/test/test_iquery.py @@ -3,15 +3,13 @@ from . import session -rodsadmins = [('otherrods', 'rods')] +rodsadmins = [] rodsusers = [('alice', 'apass')] class Test_IQuery(session.make_sessions_mixin(rodsadmins, rodsusers), unittest.TestCase): def setUp(self): super(Test_IQuery, self).setUp() - - self.admin = self.admin_sessions[0] self.user = self.user_sessions[0] def tearDown(self): @@ -21,3 +19,40 @@ def test_iquery_can_run_a_query__issue_7570(self): json_string = json.dumps([[self.user.session_collection]]) self.user.assert_icommand( ['iquery', f"select COLL_NAME where COLL_NAME = '{self.user.session_collection}'"], 'STDOUT', [json_string]) + + def test_iquery_supports_reading_query_from_stdin__issue_7570(self): + json_string = json.dumps([[self.user.session_collection]]) + self.user.assert_icommand( + ['iquery'], 'STDOUT', [json_string], input=f"select COLL_NAME where COLL_NAME = '{self.user.session_collection}'") + + def test_iquery_returns_error_on_invalid_zone__issue_7570(self): + ec, out, err = self.user.assert_icommand_fail(['iquery', '-z', 'invalid_zone', 'select COLL_NAME'], 'STDOUT') + self.assertEqual(ec, 1) + self.assertEqual(len(out), 0) + self.assertEqual(err, 'error: -26000\n') # SYS_INVALID_ZONE_NAME + + ec, out, err = self.user.assert_icommand_fail(['iquery', '-z', '', 'select COLL_NAME'], 'STDOUT') + self.assertEqual(ec, 1) + self.assertEqual(len(out), 0) + self.assertEqual(err, 'error: -26000\n') # SYS_INVALID_ZONE_NAME + + ec, out, err = self.user.assert_icommand_fail(['iquery', '-z', ' ', 'select COLL_NAME'], 'STDOUT') + self.assertEqual(ec, 1) + self.assertEqual(len(out), 0) + self.assertEqual(err, 'error: -26000\n') # SYS_INVALID_ZONE_NAME + + def test_iquery_returns_error_on_invalid_query_tokens__issue_7570(self): + ec, out, err = self.user.assert_icommand_fail(['iquery', ' '], 'STDOUT') + self.assertEqual(ec, 1) + self.assertEqual(len(out), 0) + self.assertEqual(err, 'error: -167000\n') # SYS_LIBRARY_ERROR + + ec, out, err = self.user.assert_icommand_fail(['iquery', 'select'], 'STDOUT') + self.assertEqual(ec, 1) + self.assertEqual(len(out), 0) + self.assertEqual(err, 'error: -167000\n') # SYS_LIBRARY_ERROR + + ec, out, err = self.user.assert_icommand_fail(['iquery', 'select INVALID_COLUMN'], 'STDOUT') + self.assertEqual(ec, 1) + self.assertEqual(len(out), 0) + self.assertEqual(err, 'error: -130000\n') # SYS_INVALID_INPUT_PARAM diff --git a/server/api/include/irods/rs_genquery2.hpp b/server/api/include/irods/rs_genquery2.hpp index b455a55f5b..d9b1724d8c 100644 --- a/server/api/include/irods/rs_genquery2.hpp +++ b/server/api/include/irods/rs_genquery2.hpp @@ -29,7 +29,7 @@ struct RsComm; /// /// Limitations: /// - Groups are not yet fully supported -/// - Cannot resolve tickets to data objects and collections using a single query +/// - Tickets are not yet supported. /// - Integer values must be treated as strings, except when used for OFFSET, LIMIT, FETCH FIRST \a N ROWS ONLY /// /// When the query does not include the FETCH FIRST \a N ROWS ONLY or LIMIT clause, the API will clamp the @@ -41,9 +41,9 @@ struct RsComm; /// The column mappings between GenQuery2 and the catalog can be obtained via the API. See the Genquery2Input /// structure for details. /// -/// \param[in] _comm A pointer to a RsComm. -/// \param[in] _input A pointer to a Genquery2Input. -/// \param[in,out] _output \parblock A pointer that will hold the results of the operation. +/// \param[in] _comm A pointer to a RsComm. +/// \param[in] _input A pointer to a Genquery2Input. +/// \param[out] _output \parblock A pointer that will hold the results of the operation. /// On success, the pointer will either hold a JSON string or a string representing the SQL derived from /// the GenQuery2 query string. See Genquery2Input::sql_only for details. /// diff --git a/server/api/src/rs_genquery2.cpp b/server/api/src/rs_genquery2.cpp index bd3e5b7e88..2ffb348a9f 100644 --- a/server/api/src/rs_genquery2.cpp +++ b/server/api/src/rs_genquery2.cpp @@ -2,10 +2,10 @@ #include "irods/private/genquery2_driver.hpp" #include "irods/private/genquery2_sql.hpp" +#include "irods/private/genquery2_table_column_mappings.hpp" #include "irods/apiHandler.hpp" #include "irods/catalog_utilities.hpp" -#include "irods/genquery2_table_column_mappings.hpp" #include "irods/irods_logger.hpp" #include "irods/irods_rs_comm_query.hpp" #include "irods/irods_server_properties.hpp" @@ -34,7 +34,7 @@ auto rs_genquery2(RsComm* _comm, Genquery2Input* _input, char** _output) -> int { if (!_comm || !_input || !_output) { log_api::error("{}: Invalid input: received null pointer.", __func__); - return SYS_INVALID_INPUT_PARAM; + return SYS_INTERNAL_NULL_INPUT_ERR; } // Redirect to the catalog service provider based on the user-provided zone. @@ -79,10 +79,10 @@ auto rs_genquery2(RsComm* _comm, Genquery2Input* _input, char** _output) -> int } // - // At this point, we assume we're connected to the catalog service provider. + // At this point, we assume we're connected to the catalog service provider in the correct zone. // - namespace gq = irods::experimental::api::genquery2; + namespace gq = irods::experimental::genquery2; // Generating the column mappings must happen after a redirect to guarantee the correct // mappings are returned. Remember, querying the catalog only happens on a server with direct @@ -105,7 +105,7 @@ auto rs_genquery2(RsComm* _comm, Genquery2Input* _input, char** _output) -> int if (!_input->query_string) { log_api::error("{}: Invalid input: received null pointer.", __func__); - return SYS_INVALID_INPUT_PARAM; + return SYS_INTERNAL_NULL_INPUT_ERR; } try { @@ -155,4 +155,4 @@ auto rs_genquery2(RsComm* _comm, Genquery2Input* _input, char** _output) -> int log_api::error("{}: GenQuery2 error: {}", __func__, e.what()); return SYS_LIBRARY_ERROR; } -} // rs_check_auth_credentials +} // rs_genquery2 diff --git a/server/genquery2/dsl/parser.y b/server/genquery2/dsl/parser.y index dd1aa013f7..15378b2d9e 100644 --- a/server/genquery2/dsl/parser.y +++ b/server/genquery2/dsl/parser.y @@ -54,7 +54,7 @@ This option causes make_* functions to be generated for each token kind. // According to the Bison docs, code in a %code requires/provides block will be // included in the parser header file if Bison is instructed to generate a header file. - namespace gq2_detail = irods::experimental::api::genquery2; + namespace gq2_detail = irods::experimental::genquery2; } %code { diff --git a/server/genquery2/include/irods/private/genquery2_ast_types.hpp b/server/genquery2/include/irods/private/genquery2_ast_types.hpp index 3f84d2af62..706a108e92 100644 --- a/server/genquery2/include/irods/private/genquery2_ast_types.hpp +++ b/server/genquery2/include/irods/private/genquery2_ast_types.hpp @@ -7,7 +7,7 @@ #include #include -namespace irods::experimental::api::genquery2 +namespace irods::experimental::genquery2 { struct column { @@ -206,6 +206,10 @@ namespace irods::experimental::api::genquery2 struct logical_not; struct logical_grouping; + // The name condition_type is a little wonky given the fact that nothing else + // is following that pattern. The reason "_type" is included is because the name + // "condition" is already in use. This type is an implementation detail, so it + // can be changed if it proves to be challenging to maintain. using condition_type = boost::variant; // clang-format off @@ -273,6 +277,6 @@ namespace irods::experimental::api::genquery2 range range; bool distinct = true; }; // struct select -} // namespace irods::experimental::api::genquery2 +} // namespace irods::experimental::genquery2 #endif // IRODS_GENQUERY2_ABSTRACT_SYNTAX_TREE_DATA_TYPES_HPP diff --git a/server/genquery2/include/irods/private/genquery2_driver.hpp b/server/genquery2/include/irods/private/genquery2_driver.hpp index 45e1d2463d..dade2d87ec 100644 --- a/server/genquery2/include/irods/private/genquery2_driver.hpp +++ b/server/genquery2/include/irods/private/genquery2_driver.hpp @@ -2,7 +2,7 @@ #define IRODS_GENQUERY2_DRIVER_HPP #include "irods/private/genquery2_scanner.hpp" -#include "irods/private/genquery2_ast_types.hpp" // For irods::experimental::api::genquery2::select. +#include "irods/private/genquery2_ast_types.hpp" // For irods::experimental::genquery2::select. #include "parser.hpp" // Generated by Bison. @@ -28,7 +28,7 @@ namespace irods::experimental::genquery2 auto parse(const std::string& _s) -> int; // Holds an AST-like representation of a GenQuery2 string. - irods::experimental::api::genquery2::select select; + irods::experimental::genquery2::select select; // The Flex scanner implementation. scanner lexer; diff --git a/server/genquery2/include/irods/private/genquery2_scanner.hpp b/server/genquery2/include/irods/private/genquery2_scanner.hpp index 2974ee2f4e..a6b61ff58a 100644 --- a/server/genquery2/include/irods/private/genquery2_scanner.hpp +++ b/server/genquery2/include/irods/private/genquery2_scanner.hpp @@ -16,10 +16,10 @@ namespace irods::experimental::genquery2 class scanner : public yyFlexLexer { public: - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Woverloaded-virtual" +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Woverloaded-virtual" auto yylex(driver& _driver) -> yy::parser::symbol_type; - #pragma clang diagnostic pop +#pragma clang diagnostic pop }; // class scanner } // namespace irods::experimental::genquery2 diff --git a/server/genquery2/include/irods/private/genquery2_sql.hpp b/server/genquery2/include/irods/private/genquery2_sql.hpp index b512d08a13..59c299aa4a 100644 --- a/server/genquery2/include/irods/private/genquery2_sql.hpp +++ b/server/genquery2/include/irods/private/genquery2_sql.hpp @@ -5,7 +5,7 @@ #include #include -namespace irods::experimental::api::genquery2 +namespace irods::experimental::genquery2 { struct select; @@ -19,6 +19,6 @@ namespace irods::experimental::api::genquery2 }; // struct options auto to_sql(const select& _select, const options& _opts) -> std::tuple>; -} // namespace irods::experimental::api::genquery2 +} // namespace irods::experimental::genquery2 #endif // IRODS_GENQUERY2_SQL_HPP diff --git a/server/genquery2/include/irods/genquery2_table_column_mappings.hpp b/server/genquery2/include/irods/private/genquery2_table_column_mappings.hpp similarity index 95% rename from server/genquery2/include/irods/genquery2_table_column_mappings.hpp rename to server/genquery2/include/irods/private/genquery2_table_column_mappings.hpp index 5abe85fa03..01555f6b78 100644 --- a/server/genquery2/include/irods/genquery2_table_column_mappings.hpp +++ b/server/genquery2/include/irods/private/genquery2_table_column_mappings.hpp @@ -4,7 +4,7 @@ #include #include -namespace irods::experimental::api::genquery2 +namespace irods::experimental::genquery2 { struct column_info { @@ -66,8 +66,8 @@ namespace irods::experimental::api::genquery2 {"DATA_TYPE_NAME", {"R_DATA_MAIN", "data_type_name"}}, {"DATA_SIZE", {"R_DATA_MAIN", "data_size"}}, {"DATA_PATH", {"R_DATA_MAIN", "data_path"}}, - //{"DATA_OWNER_NAME", {"R_DATA_MAIN", "data_owner_name"}}, // Misleading. Prefer DATA_USER_NAME. - //{"DATA_OWNER_ZONE", {"R_DATA_MAIN", "data_owner_zone"}}, // Misleading. Prefer DATA_USER_ZONE. + //{"DATA_OWNER_NAME", {"R_DATA_MAIN", "data_owner_name"}}, // Misleading. Use DATA_ACCESS_USER_NAME. + //{"DATA_OWNER_ZONE", {"R_DATA_MAIN", "data_owner_zone"}}, // Misleading. Use DATA_ACCESS_USER_ZONE. {"DATA_REPL_STATUS", {"R_DATA_MAIN", "data_is_dirty"}}, {"DATA_STATUS", {"R_DATA_MAIN", "data_status"}}, {"DATA_CHECKSUM", {"R_DATA_MAIN", "data_checksum"}}, @@ -90,8 +90,8 @@ namespace irods::experimental::api::genquery2 {"COLL_ID", {"R_COLL_MAIN", "coll_id"}}, {"COLL_NAME", {"R_COLL_MAIN", "coll_name"}}, {"COLL_PARENT_NAME", {"R_COLL_MAIN", "parent_coll_name"}}, - //{"COLL_OWNER_NAME", {"R_COLL_MAIN", "coll_owner_name"}}, // Misleading. Prefer COLL_USER_NAME. - //{"COLL_OWNER_ZONE", {"R_COLL_MAIN", "coll_owner_zone"}}, // Misleading. Prefer COLL_USER_ZONE. + //{"COLL_OWNER_NAME", {"R_COLL_MAIN", "coll_owner_name"}}, // Misleading. Use COLL_ACCESS_USER_NAME. + //{"COLL_OWNER_ZONE", {"R_COLL_MAIN", "coll_owner_zone"}}, // Misleading. Use COLL_ACCESS_USER_ZONE. {"COLL_MAP_ID", {"R_COLL_MAIN", "coll_map_id"}}, {"COLL_INHERITANCE", {"R_COLL_MAIN", "coll_inheritance"}}, {"COLL_COMMENTS", {"R_COLL_MAIN", "r_comment"}}, @@ -180,7 +180,8 @@ namespace irods::experimental::api::genquery2 {"DATA_ACCESS_PERM_ID", {"R_OBJT_ACCESS", "access_type_id"}}, {"DATA_ACCESS_PERM_NAME", {"R_TOKN_MAIN", "token_name"}}, {"DATA_ACCESS_USER_ID", {"R_OBJT_ACCESS", "user_id"}}, - {"DATA_ACCESS_USER_NAME", {"R_USER_MAIN", "user_name"}}, + {"DATA_ACCESS_USER_NAME", {"R_USER_MAIN", "user_name"}}, // What about groups? + {"DATA_ACCESS_USER_ZONE", {"R_USER_MAIN", "user_zone"}}, //{"DATA_ACCESS_DATA_ID", {"R_OBJT_ACCESS", "object_id"}}, //{"DATA_ACCESS_NAME", {"R_TOKN_MAIN", "token_name"}}, // special? //{"DATA_TOKEN_NAMESPACE", {"R_TOKN_MAIN", "token_namespace"}}, // special? @@ -189,6 +190,7 @@ namespace irods::experimental::api::genquery2 {"COLL_ACCESS_PERM_NAME", {"R_TOKN_MAIN", "token_name"}}, {"COLL_ACCESS_USER_ID", {"R_OBJT_ACCESS", "user_id"}}, {"COLL_ACCESS_USER_NAME", {"R_USER_NAME", "user_name"}}, + {"COLL_ACCESS_USER_ZONE", {"R_USER_NAME", "user_zone"}}, //{"COLL_ACCESS_COLL_ID", {"R_OBJT_ACCESS", "object_id"}}, //{"COLL_ACCESS_NAME", {"R_TOKN_MAIN", "token_name"}}, // special? //{"COLL_TOKEN_NAMESPACE", {"R_TOKN_MAIN", "token_namespace"}}, // special? @@ -210,7 +212,10 @@ namespace irods::experimental::api::genquery2 //{"TICKET_EXPIRY_TIME", {"R_TICKET_MAIN", "ticket_expiry_ts"}}, //{"TICKET_CREATE_TIME", {"R_TICKET_MAIN", "create_time"}}, //{"TICKET_MODIFY_TIME", {"R_TICKET_MAIN", "modify_time"}}, - ////{"TICKET_LOGICAL_PATH", {"R_TICKET_MAIN", "modify_time"}}, + + // Is this possible? Does it make sense? + // This isn't a thing in GenQuery1. + //{"TICKET_LOGICAL_PATH", {"R_TICKET_MAIN", "modify_time"}}, //{"TICKET_ALLOWED_HOST", {"R_TICKET_ALLOWED_HOSTS", "host"}}, //{"TICKET_ALLOWED_HOST_TICKET_ID", {"R_TICKET_ALLOWED_HOSTS", "ticket_id"}}, @@ -228,6 +233,6 @@ namespace irods::experimental::api::genquery2 //{"TICKET_DATA_COLL_NAME", {"R_COLL_MAIN", "coll_name"}} // Includes join between R_DATA_MAIN and //R_COLL_MAIN. What is this? }; // column_name_mappings -} // namespace irods::experimental::api::genquery2 +} // namespace irods::experimental::genquery2 #endif // IRODS_GENQUERY2_TABLE_COLUMN_MAPPINGS_HPP diff --git a/server/genquery2/src/genquery2_sql.cpp b/server/genquery2/src/genquery2_sql.cpp index 1377063379..a0db642a45 100644 --- a/server/genquery2/src/genquery2_sql.cpp +++ b/server/genquery2/src/genquery2_sql.cpp @@ -3,7 +3,7 @@ #include "irods/private/genquery2_ast_types.hpp" #include "irods/private/vertex_property.hpp" #include "irods/private/edge_property.hpp" -#include "irods/genquery2_table_column_mappings.hpp" +#include "irods/private/genquery2_table_column_mappings.hpp" #include "irods/irods_at_scope_exit.hpp" #include "irods/irods_logger.hpp" @@ -24,7 +24,7 @@ namespace { - namespace gq = irods::experimental::api::genquery2; + namespace gq = irods::experimental::genquery2; // clang-format off using graph_type = boost::adjacency_list std::size_t { - for (auto i = 0ull; i < table_names.size(); ++i) { - if (table_names[i] == _table_name) { + for (auto i = 0ULL; i < table_names.size(); ++i) { + if (table_names.at(i) == _table_name) { return i; } } @@ -255,7 +255,7 @@ namespace std::vector processed; processed.reserve(_tables.size()); - processed.push_back(std::string{_tables.front()}); + processed.emplace_back(std::string{_tables.front()}); log_gq::debug(fmt::format("processed = [{}]", fmt::join(processed, ", "))); for (decltype(_tables.size()) i = 0; i < _tables.size() - 1; ++i) { @@ -428,21 +428,20 @@ namespace " and pdoa.access_type_id >= {perm} and pcoa.access_type_id >= {perm}", fmt::arg("perm", min_perm_level), fmt::arg("zone", _opts.user_zone)); - _state.values.push_back(std::string{_opts.user_name}); - _state.values.push_back(std::string{_opts.user_name}); + _state.values.emplace_back(std::string{_opts.user_name}); + _state.values.emplace_back(std::string{_opts.user_name}); } else if (d_iter != end) { sql += fmt::format(" and pdu.user_name = ? and pdu.zone_name = '{}' and pdoa.access_type_id >= {}", _opts.user_zone, min_perm_level); - _state.values.push_back(std::string{_opts.user_name}); - _state.values.push_back(std::string{_opts.user_zone}); + _state.values.emplace_back(std::string{_opts.user_name}); } else if (c_iter != end) { sql += fmt::format(" and pcu.user_name = ? and pcu.zone_name = '{}' and pcoa.access_type_id >= {}", _opts.user_zone, min_perm_level); - _state.values.push_back(std::string{_opts.user_name}); + _state.values.emplace_back(std::string{_opts.user_name}); } } @@ -472,20 +471,20 @@ namespace " and pdoa.access_type_id >= {perm} and pcoa.access_type_id >= {perm}", fmt::arg("perm", min_perm_level), fmt::arg("zone", _opts.user_zone)); - _state.values.push_back(std::string{_opts.user_name}); - _state.values.push_back(std::string{_opts.user_name}); + _state.values.emplace_back(std::string{_opts.user_name}); + _state.values.emplace_back(std::string{_opts.user_name}); } else if (d_iter != end) { sql += fmt::format(" where pdu.user_name = ? and pdu.zone_name = '{}' and pdoa.access_type_id >= {}", _opts.user_zone, min_perm_level); - _state.values.push_back(std::string{_opts.user_name}); + _state.values.emplace_back(std::string{_opts.user_name}); } else if (c_iter != end) { sql += fmt::format(" where pcu.user_name = ? and pcu.zone_name = '{}' and pcoa.access_type_id >= {}", _opts.user_zone, min_perm_level); - _state.values.push_back(std::string{_opts.user_name}); + _state.values.emplace_back(std::string{_opts.user_name}); } } @@ -530,7 +529,7 @@ namespace const auto ast_iter = std::find_if(std::begin(_state.ast_column_ptrs), std::end(_state.ast_column_ptrs), - [&c](const irods::experimental::api::genquery2::column* _p) { return _p->name == c; }); + [&c](const irods::experimental::genquery2::column* _p) { return _p->name == c; }); if (std::end(_state.ast_column_ptrs) == ast_iter) { throw std::invalid_argument{"cannot generate SQL from General Query."}; @@ -586,10 +585,10 @@ namespace alias = _state.table_aliases.at(std::string{iter->second.table}); } - const auto ast_iter = std::find_if( - std::begin(_state.ast_column_ptrs), - std::end(_state.ast_column_ptrs), - [&se](const irods::experimental::api::genquery2::column* _p) { return _p->name == se.column; }); + const auto ast_iter = + std::find_if(std::begin(_state.ast_column_ptrs), + std::end(_state.ast_column_ptrs), + [&se](const irods::experimental::genquery2::column* _p) { return _p->name == se.column; }); if (std::end(_state.ast_column_ptrs) == ast_iter) { throw std::invalid_argument{"cannot generate SQL from General Query."}; @@ -710,7 +709,7 @@ namespace fmt::arg("char_type", "varchar")); } // generate_with_clause_for_data_resc_hier - auto generate_limit_clause(const irods::experimental::api::genquery2::options& _opts, + auto generate_limit_clause(const irods::experimental::genquery2::options& _opts, const std::string_view _number_of_rows) -> std::string { if (!_number_of_rows.empty()) { @@ -729,7 +728,7 @@ namespace } // generate_limit_clause } // anonymous namespace -namespace irods::experimental::api::genquery2 +namespace irods::experimental::genquery2 { auto setup_column_for_post_processing(gq_state& _state, const column& _column, const column_info& _column_info) -> std::tuple @@ -862,7 +861,7 @@ namespace irods::experimental::api::genquery2 } return fmt::format("cast({}.{} as {})", alias, iter->second.name, _column.type_name); - } + } // to_sql auto to_sql(gq_state& _state, const select_function& _select_function) -> std::string { @@ -895,7 +894,7 @@ namespace irods::experimental::api::genquery2 alias, iter->second.name, _select_function.column.type_name); - } + } // to_sql auto to_sql(gq_state& _state, const selections& _selections) -> std::string { @@ -921,55 +920,55 @@ namespace irods::experimental::api::genquery2 } return fmt::format("{}", fmt::join(cols, ", ")); - } + } // to_sql auto to_sql(gq_state& _state, const condition_operator_not& _op_not) -> std::string { return fmt::format(" not{}", boost::apply_visitor(sql_visitor{_state}, _op_not.expression)); - } + } // to_sql auto to_sql(gq_state& _state, const condition_not_equal& _not_equal) -> std::string { _state.values.push_back(_not_equal.string_literal); return " != ?"; - } + } // to_sql auto to_sql(gq_state& _state, const condition_equal& _equal) -> std::string { _state.values.push_back(_equal.string_literal); return " = ?"; - } + } // to_sql auto to_sql(gq_state& _state, const condition_less_than& _less_than) -> std::string { _state.values.push_back(_less_than.string_literal); return " < ?"; - } + } // to_sql auto to_sql(gq_state& _state, const condition_less_than_or_equal_to& _less_than_or_equal_to) -> std::string { _state.values.push_back(_less_than_or_equal_to.string_literal); return " <= ?"; - } + } // to_sql auto to_sql(gq_state& _state, const condition_greater_than& _greater_than) -> std::string { _state.values.push_back(_greater_than.string_literal); return " > ?"; - } + } // to_sql auto to_sql(gq_state& _state, const condition_greater_than_or_equal_to& _greater_than_or_equal_to) -> std::string { _state.values.push_back(_greater_than_or_equal_to.string_literal); return " >= ?"; - } + } // to_sql auto to_sql(gq_state& _state, const condition_between& _between) -> std::string { _state.values.push_back(_between.low); _state.values.push_back(_between.high); return " between ? and ?"; - } + } // to_sql auto to_sql(gq_state& _state, const condition_in& _in) -> std::string { @@ -991,30 +990,30 @@ namespace irods::experimental::api::genquery2 auto last = boost::make_function_input_iterator(gen, _in.list_of_string_literals.size()); return fmt::format(" in ({})", fmt::join(first, last, ", ")); - } + } // to_sql auto to_sql(gq_state& _state, const condition_like& _like) -> std::string { _state.values.push_back(_like.string_literal); return " like ?"; - } + } // to_sql auto to_sql([[maybe_unused]] gq_state& _state, const condition_is_null&) -> std::string { return " is null"; - } + } // to_sql auto to_sql([[maybe_unused]] gq_state& _state, const condition_is_not_null&) -> std::string { return " is not null"; - } + } // to_sql auto to_sql(gq_state& _state, const condition& _condition) -> std::string { return fmt::format("{}{}", to_sql(_state, _condition.column), boost::apply_visitor(sql_visitor{_state}, _condition.expression)); - } + } // to_sql auto to_sql(gq_state& _state, const conditions& _conditions) -> std::string { @@ -1025,27 +1024,27 @@ namespace irods::experimental::api::genquery2 } return ret; - } + } // to_sql auto to_sql(gq_state& _state, const logical_and& _condition) -> std::string { return fmt::format(" and {}", to_sql(_state, _condition.condition)); - } + } // to_sql auto to_sql(gq_state& _state, const logical_or& _condition) -> std::string { return fmt::format(" or {}", to_sql(_state, _condition.condition)); - } + } // to_sql auto to_sql(gq_state& _state, const logical_not& _condition) -> std::string { return fmt::format("not {}", to_sql(_state, _condition.condition)); - } + } // to_sql auto to_sql(gq_state& _state, const logical_grouping& _condition) -> std::string { return fmt::format("({})", to_sql(_state, _condition.conditions)); - } + } // to_sql auto to_sql(const select& _select, const options& _opts) -> std::tuple> { @@ -1169,4 +1168,4 @@ namespace irods::experimental::api::genquery2 return {{}, {}}; } // to_sql -} // namespace irods::experimental::api::genquery2 +} // namespace irods::experimental::genquery2 diff --git a/server/icat/include/irods/icatHighLevelRoutines.hpp b/server/icat/include/irods/icatHighLevelRoutines.hpp index 13a69191b7..2d3d66f905 100644 --- a/server/icat/include/irods/icatHighLevelRoutines.hpp +++ b/server/icat/include/irods/icatHighLevelRoutines.hpp @@ -398,10 +398,10 @@ auto chl_check_auth_credentials(RsComm& _comm, /// /// Triggers policy associated with database operations. /// -/// \param[in] _comm The communication object. -/// \param[in] _sql The SQL, generated by the GenQuery2 parser, to execute. -/// \param[in] _values The list of values to bind to the query. -/// \param[in,out] _output The pointer that will hold the results of the query. +/// \param[in] _comm The communication object. +/// \param[in] _sql The SQL, generated by the GenQuery2 parser, to execute. +/// \param[in] _values The list of values to bind to the query. +/// \param[out] _output The pointer that will hold the results of the query. /// /// \return An integer. /// \retval 0 On success. diff --git a/server/re/include/irods/msi_genquery2.hpp b/server/re/include/irods/msi_genquery2.hpp index 40bf46c808..9869bfbe7c 100644 --- a/server/re/include/irods/msi_genquery2.hpp +++ b/server/re/include/irods/msi_genquery2.hpp @@ -6,22 +6,118 @@ struct MsParam; struct RuleExecInfo; -/// TODO +/// Query the catalog using the GenQuery2 parser. +/// +/// \warning This microservice is experimental and may change in the future. +/// \warning This microservice is NOT thread-safe. +/// +/// GenQuery2 handles are only available to the agent which produced them. +/// +/// The lifetime of a handle and its resultset is tied to the lifetime of the agent. +/// The value of the handle MUST NOT be viewed as having any meaning. Policy MUST NOT rely on +/// its structure for any reason as it may change across releases. +/// +/// \param[out] _handle The output parameter that will hold the handle to the resultset. +/// \param[in] _query_string The GenQuery2 string to execute. +/// \param[in] _rei This parameter is special and should be ignored. +/// +/// \return An integer. +/// \retval 0 On success. +/// \retval <0 On failure. +/// +/// \see genquery2.h for features supported by GenQuery2. /// /// \since 4.3.2 auto msi_genquery2_execute(MsParam* _handle, MsParam* _query_string, RuleExecInfo* _rei) -> int; -/// TODO +/// Moves the cursor forward by one row. +/// +/// \warning This microservice is experimental and may change in the future. +/// \warning This microservice is NOT thread-safe. +/// +/// GenQuery2 handles are only available to the agent which produced them. +/// +/// \param[in] _handle The GenQuery2 handle. +/// \param[in] _rei This parameter is special and should be ignored. +/// +/// \return An integer. +/// \retval 0 If data can be read from the new row. +/// \retval -408000 If the end of the resultset has been reached. +/// \retval <0 If an error occurred. +/// +/// \b Example +/// \code{.py} +/// # A global constant representing the value which indicates the end of +/// # the resultset has been reached. +/// END_OF_RESULTSET = -408000; +/// +/// iterating_over_genquery2_results() +/// { +/// # Execute a query. The results are stored in the Rule Engine Plugin. +/// msi_genquery2_execute(*handle, "select COLL_NAME, DATA_NAME order by DATA_NAME desc limit 1"); +/// +/// # Iterate over the results. +/// while (true) { +/// *ec = errorcode(msi_genquery2_next_row(*handle)); +/// +/// if (*ec < 0) { +/// if (END_OF_RESULTSET == *ec) { +/// break; +/// } +/// +/// failmsg(*ec, "Unexpected error while iterating over GenQuery2 resultset."); +/// } +/// +/// msi_genquery2_column(*handle, '0', *coll_name); # Copy the COLL_NAME into *coll_name. +/// msi_genquery2_column(*handle, '1', *data_name); # Copy the DATA_NAME into *data_name. +/// writeLine("stdout", "logical path => [*coll_name/*data_name]"); +/// } +/// +/// # Free any resources used. This is handled for you when the agent is shut down as well. +/// msi_genquery2_free(*handle); +/// } +/// \endcode /// /// \since 4.3.2 auto msi_genquery2_next_row(MsParam* _handle, RuleExecInfo* _rei) -> int; -/// TODO +/// Reads the value of a column from a row within a GenQuery2 resultset. +/// +/// \warning This microservice is experimental and may change in the future. +/// \warning This microservice is NOT thread-safe. +/// +/// GenQuery2 handles are only available to the agent which produced them. +/// +/// \param[in] _handle The GenQuery2 handle. +/// \param[in] _column_index The index of the column to read. The index must be passed as a string. +/// \param[out] _column_value The variable to write the value of the column to. +/// \param[in] _rei This parameter is special and should be ignored. +/// +/// \return An integer. +/// \retval 0 On success. +/// \retval <0 On failure. /// /// \since 4.3.2 auto msi_genquery2_column(MsParam* _handle, MsParam* _column_index, MsParam* _column_value, RuleExecInfo* _rei) -> int; -/// TODO +/// Frees all resources associated with a GenQuery2 handle. +/// +/// \warning This microservice is experimental and may change in the future. +/// \warning This microservice is NOT thread-safe. +/// +/// GenQuery2 handles are only available to the agent which produced them. +/// +/// Users are expected to call this microservice when a GenQuery2 resultset is no longer needed. +/// +/// \warning Failing to follow this rule can result in memory leaks. However, the handle and the resultset +/// it is mapped to will be free'd on agent teardown. +/// +/// \param[in] _handle The GenQuery2 handle. +/// \param[in] _rei This parameter is special and should be ignored. +/// +/// \return An integer. +/// \retval 0 On success. +/// \retval <0 On failure. /// /// \since 4.3.2 auto msi_genquery2_free(MsParam* _handle, RuleExecInfo* _rei) -> int; diff --git a/server/re/src/irods_re_serialization.cpp b/server/re/src/irods_re_serialization.cpp index 24e7fb0041..6415234621 100644 --- a/server/re/src/irods_re_serialization.cpp +++ b/server/re/src/irods_re_serialization.cpp @@ -1,11 +1,19 @@ #include "irods/irods_re_serialization.hpp" +#include "irods/irods_logger.hpp" #include "irods/irods_plugin_context.hpp" #include "irods/rodsErrorTable.h" #include +#include + #include +namespace +{ + using log_re = irods::experimental::log::rule_engine; +} // anonymous namespace + namespace irods::re_serialization { static void serialize_keyValPair( @@ -1112,6 +1120,85 @@ namespace irods::re_serialization return SUCCESS(); } // serialize_bytesBuf_ptr + static irods::error serialize_Genquery2Input_ptr(boost::any _p, serialized_parameter_t& _out) + { + try { + const auto* v = boost::any_cast(_p); + + _out["query_string"] = v->query_string ? v->query_string : ""; + _out["zone"] = v->zone ? v->zone : ""; + _out["sql_only"] = std::to_string(v->sql_only); + _out["column_mappings"] = std::to_string(v->column_mappings); + + return SUCCESS(); + } + catch (const boost::bad_any_cast& e) { + return ERROR(INVALID_ANY_CAST, + fmt::format("{}: failed to cast pointer to [Genquery2Input*]: {}", __func__, e.what())); + } + catch (const std::exception& e) { + return ERROR( + SYS_LIBRARY_ERROR, fmt::format("{}: failed to serialize [Genquery2Input*]: {}", __func__, e.what())); + } + } // serialize_Genquery2Input_ptr + + static void serialize_vector_of_strings_ptr_impl(const std::vector& _v, serialized_parameter_t& _out) + { + _out["size"] = std::to_string(_v.size()); + + for (auto&& e : _v | boost::adaptors::indexed()) { + log_re::debug("{}: element = {{index: {}, value: {}}}", __func__, e.index(), e.value()); + _out[std::to_string(e.index())] = e.value(); + } + } // serialize_vector_of_strings_ptr_impl + + // Serializes a const vector of strings. + // + // Let *v represent the vector of strings in the NREP. + // To get the number of elements in the vector, use: *v."size". The count will be of type string. + // To get an element via its index, use: *v."". The value will be of type string. + static irods::error serialize_const_vector_of_strings_ptr(boost::any _p, serialized_parameter_t& _out) + { + try { + log_re::trace(__func__); + serialize_vector_of_strings_ptr_impl(*boost::any_cast*>(_p), _out); + return SUCCESS(); + } + catch (const boost::bad_any_cast& e) { + return ERROR( + INVALID_ANY_CAST, + fmt::format("{}: failed to cast pointer to [const std::vector*]: {}", __func__, e.what())); + } + catch (const std::exception& e) { + return ERROR( + SYS_LIBRARY_ERROR, + fmt::format("{}: failed to serialize [const std::vector*]: {}", __func__, e.what())); + } + } // serialize_const_vector_of_strings_ptr + + // Serializes a (non-const) vector of strings. + // + // Let *v represent the vector of strings in the NREP. + // To get the number of elements in the vector, use: *v."size". The count will be of type string. + // To get an element via its index, use: *v."". The value will be of type string. + static irods::error serialize_vector_of_strings_ptr(boost::any _p, serialized_parameter_t& _out) + { + try { + log_re::trace(__func__); + serialize_vector_of_strings_ptr_impl(*boost::any_cast*>(_p), _out); + return SUCCESS(); + } + catch (const boost::bad_any_cast& e) { + return ERROR( + INVALID_ANY_CAST, + fmt::format("{}: failed to cast pointer to [std::vector*]: {}", __func__, e.what())); + } + catch (const std::exception& e) { + return ERROR(SYS_LIBRARY_ERROR, + fmt::format("{}: failed to serialize [std::vector*]: {}", __func__, e.what())); + } + } // serialize_vector_of_strings_ptr + #if 0 static error serialize_XXXX_ptr( boost::any _p, @@ -1165,7 +1252,10 @@ namespace irods::re_serialization { std::type_index(typeid(char**)), serialize_char_ptr_ptr }, { std::type_index(typeid(openedDataObjInp_t*)), serialize_openedDataObjInp_ptr }, { std::type_index(typeid(openedDataObjInp_t**)), serialize_openedDataObjInp_ptr_ptr }, - { std::type_index(typeid(bytesBuf_t*)), serialize_bytesBuf_ptr } + { std::type_index(typeid(bytesBuf_t*)), serialize_bytesBuf_ptr }, + { std::type_index(typeid(Genquery2Input*)), serialize_Genquery2Input_ptr }, + { std::type_index(typeid(const std::vector*)), serialize_const_vector_of_strings_ptr }, + { std::type_index(typeid(std::vector*)), serialize_vector_of_strings_ptr } }; return the_map; diff --git a/server/re/src/msi_genquery2.cpp b/server/re/src/msi_genquery2.cpp index 7e43303b3a..fe5fa1b8c0 100644 --- a/server/re/src/msi_genquery2.cpp +++ b/server/re/src/msi_genquery2.cpp @@ -37,28 +37,6 @@ namespace std::map gq2_context; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // anonymous namespace -/// Query the catalog using the GenQuery2 parser. -/// -/// \warning This microservice is experimental and may change in the future. -/// \warning This microservice is NOT thread-safe. -/// -/// GenQuery2 handles are only available to the agent which produced them. -/// -/// The lifetime of a handle and its resultset is tied to the lifetime of the agent. -/// The value of the handle MUST NOT be viewed as having any meaning. Policy MUST NOT rely on -/// its structure for any reason as it may change across releases. -/// -/// \param[in,out] _handle The output parameter that will hold the handle to the resultset. -/// \param[in] _query_string The GenQuery2 string to execute. -/// \param[in] _rei This parameter is special and should be ignored. -/// -/// \return An integer. -/// \retval 0 On success. -/// \retval <0 On failure. -/// -/// \see genquery2.h for features supported by GenQuery2. -/// -/// \since 4.3.2 auto msi_genquery2_execute(MsParam* _handle, MsParam* _query_string, RuleExecInfo* _rei) -> int { log_msi::trace(__func__); @@ -100,55 +78,6 @@ auto msi_genquery2_execute(MsParam* _handle, MsParam* _query_string, RuleExecInf return 0; } // msi_genquery2_execute -/// Moves the cursor forward by one row. -/// -/// \warning This microservice is experimental and may change in the future. -/// \warning This microservice is NOT thread-safe. -/// -/// GenQuery2 handles are only available to the agent which produced them. -/// -/// \param[in] _handle The GenQuery2 handle. -/// \param[in] _rei This parameter is special and should be ignored. -/// -/// \return An integer. -/// \retval 0 If data can be read from the new row. -/// \retval -408000 If the end of the resultset has been reached. -/// \retval <0 If an error occurred. -/// -/// \b Example -/// \code{.py} -/// # A global constant representing the value which indicates the end of -/// # the resultset has been reached. -/// END_OF_RESULTSET = -408000; -/// -/// iterating_over_genquery2_results() -/// { -/// # Execute a query. The results are stored in the Rule Engine Plugin. -/// msi_genquery2_execute(*handle, "select COLL_NAME, DATA_NAME order by DATA_NAME desc limit 1"); -/// -/// # Iterate over the results. -/// while (true) { -/// *ec = errorcode(msi_genquery2_next_row(*handle)); -/// -/// if (*ec < 0) { -/// if (END_OF_RESULTSET == *ec) { -/// break; -/// } -/// -/// failmsg(*ec, "Unexpected error while iterating over GenQuery2 resultset."); -/// } -/// -/// msi_genquery2_column(*handle, '0', *coll_name); # Copy the COLL_NAME into *coll_name. -/// msi_genquery2_column(*handle, '1', *data_name); # Copy the DATA_NAME into *data_name. -/// writeLine("stdout", "logical path => [*coll_name/*data_name]"); -/// } -/// -/// # Free any resources used. This is handled for you when the agent is shut down as well. -/// msi_genquery2_free(*handle); -/// } -/// \endcode -/// -/// \since 4.3.2 auto msi_genquery2_next_row(MsParam* _handle, RuleExecInfo* _rei) -> int { log_msi::trace(__func__); @@ -174,6 +103,7 @@ auto msi_genquery2_next_row(MsParam* _handle, RuleExecInfo* _rei) -> int if (std::end(gq2_context) == iter) { log_msi::error("{}: Unknown context handle.", __func__); + return SYS_INVALID_INPUT_PARAM; } auto& ctx = iter->second; @@ -200,23 +130,6 @@ auto msi_genquery2_next_row(MsParam* _handle, RuleExecInfo* _rei) -> int } } // msi_genquery2_next_row -/// Reads the value of a column from a row within a GenQuery2 resultset. -/// -/// \warning This microservice is experimental and may change in the future. -/// \warning This microservice is NOT thread-safe. -/// -/// GenQuery2 handles are only available to the agent which produced them. -/// -/// \param[in] _handle The GenQuery2 handle. -/// \param[in] _column_index The index of the column to read. The index must be passed as a string. -/// \param[in,out] _column_value The variable to write the value of the column to. -/// \param[in] _rei This parameter is special and should be ignored. -/// -/// \return An integer. -/// \retval 0 On success. -/// \retval <0 On failure. -/// -/// \since 4.3.2 auto msi_genquery2_column(MsParam* _handle, MsParam* _column_index, MsParam* _column_value, RuleExecInfo* _rei) -> int { log_msi::trace(__func__); @@ -247,6 +160,7 @@ auto msi_genquery2_column(MsParam* _handle, MsParam* _column_index, MsParam* _co if (std::end(gq2_context) == iter) { log_msi::error("{}: Unknown context handle.", __func__); + return SYS_INVALID_INPUT_PARAM; } auto& ctx = iter->second; @@ -264,25 +178,6 @@ auto msi_genquery2_column(MsParam* _handle, MsParam* _column_index, MsParam* _co } } // msi_genquery2_column -/// Frees all resources associated with a GenQuery2 handle. -/// -/// \warning This microservice is experimental and may change in the future. -/// \warning This microservice is NOT thread-safe. -/// -/// GenQuery2 handles are only available to the agent which produced them. -/// -/// Users are expected to call this microservice when a GenQuery2 resultset is no longer needed. -/// Failing to follow this rule can result in memory leaks. However, the handle and the resultset -/// it is mapped to will be free'd on agent teardown. -/// -/// \param[in] _handle The GenQuery2 handle. -/// \param[in] _rei This parameter is special and should be ignored. -/// -/// \return An integer. -/// \retval 0 On success. -/// \retval <0 On failure. -/// -/// \since 4.3.2 auto msi_genquery2_free(MsParam* _handle, RuleExecInfo* _rei) -> int { log_msi::trace(__func__); @@ -304,7 +199,14 @@ auto msi_genquery2_free(MsParam* _handle, RuleExecInfo* _rei) -> int return SYS_INVALID_INPUT_PARAM; } - gq2_context.erase(ctx_handle_index); + auto iter = gq2_context.find(ctx_handle_index); + + if (std::end(gq2_context) == iter) { + log_msi::error("{}: Unknown context handle.", __func__); + return SYS_INVALID_INPUT_PARAM; + } + + gq2_context.erase(iter); return 0; }