From 165182cdbf869fc316de6b1d1bf51cbcf392e264 Mon Sep 17 00:00:00 2001 From: ljkimmel <31107861+ljkimmel@users.noreply.github.com> Date: Sun, 7 May 2023 14:58:38 -0500 Subject: [PATCH] mssql_script: allow non-returning SQL statements (#6457) * feat: Allow non-returning SQL statements - The current implementation fails out when certain statements or batches do not have resultsets - this limits the usefulness of the module - Instead, it is known that statements without resultsets return then OperationalError exception with text "Statement not executed or executed statement has no resultset". We will utilize these facts to accept these statements - The implementation also assumes that users will always use best- practices for the script syntax; that is, "GO" will always be capitalized but this is not strictly required -- update to allow "GO" to be any mixed-case Signed-off-by: Lesley Kimmel * feat: Add changelog fragment for change - Add changelog fragment for PR 6192 Signed-off-by: Lesley Kimmel * feat: Improve batching - Previous batching had shortcomings like making strict assumptions about the format of the incoming script and did not handle Windows- based scripts (e.g. \r characters). It also did not handle cases where there were trailing or leading whitespace characters round the 'GO' - Added a special case for removing the Byte Order Mark (BOM) character that may come as part of a script when slurped from some hosts. Signed-off-by: Lesley Kimmel * feat: Use str.splitlines() - Use of this method is cleaner Signed-off-by: Lesley Kimmel * Update changelogs/fragments/6192-allow-empty-resultsets.yml Co-authored-by: Felix Fontein * fix: Update transcribing errors - Replace local namespace with project namespace - Remove 'return' statement from the module.fail_json call Signed-off-by: Lesley Kimmel --------- Signed-off-by: Lesley Kimmel Co-authored-by: Lesley Kimmel Co-authored-by: Felix Fontein --- .../fragments/6192-allow-empty-resultsets.yml | 4 ++ plugins/modules/mssql_script.py | 37 ++++++++++++++++--- .../targets/mssql_script/tasks/main.yml | 37 +++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 changelogs/fragments/6192-allow-empty-resultsets.yml diff --git a/changelogs/fragments/6192-allow-empty-resultsets.yml b/changelogs/fragments/6192-allow-empty-resultsets.yml new file mode 100644 index 0000000000..9085d460c8 --- /dev/null +++ b/changelogs/fragments/6192-allow-empty-resultsets.yml @@ -0,0 +1,4 @@ +minor_changes: + - mssql_script - handle error condition for empty resultsets to allow for non-returning SQL statements (for example ``UPDATE`` and ``INSERT``) (https://github.com/ansible-collections/community.general/pull/6457). + - mssql_script - allow for ``GO`` statement to be mixed-case for scripts not using strict syntax (https://github.com/ansible-collections/community.general/pull/6457). + - mssql_script - improve batching logic to allow a wider variety of input scripts. For example, SQL scripts slurped from Windows machines which may contain carriage return (''\r'') characters (https://github.com/ansible-collections/community.general/pull/6457). \ No newline at end of file diff --git a/plugins/modules/mssql_script.py b/plugins/modules/mssql_script.py index 1696000db2..715f2faf04 100644 --- a/plugins/modules/mssql_script.py +++ b/plugins/modules/mssql_script.py @@ -280,14 +280,32 @@ def run_module(): cursor = conn.cursor(as_dict=True) query_results_key = 'query_results_dict' - queries = script.split('\nGO\n') + # Process the script into batches + queries = [] + current_batch = [] + for statement in script.splitlines(keepends=True): + # Ignore the Byte Order Mark, if found + if statement.strip() == '\uFEFF': + continue + + # Assume each 'GO' is on its own line but may have leading/trailing whitespace + # and be of mixed-case + if statement.strip().upper() != 'GO': + current_batch.append(statement) + else: + queries.append(''.join(current_batch)) + current_batch = [] + if len(current_batch) > 0: + queries.append(''.join(current_batch)) + result['changed'] = True if module.check_mode: module.exit_json(**result) query_results = [] - try: - for query in queries: + for query in queries: + # Catch and exit on any bad query errors + try: cursor.execute(query, sql_params) qry_result = [] rows = cursor.fetchall() @@ -295,8 +313,17 @@ def run_module(): qry_result.append(rows) rows = cursor.fetchall() query_results.append(qry_result) - except Exception as e: - return module.fail_json(msg="query failed", query=query, error=str(e), **result) + except Exception as e: + # We know we executed the statement so this error just means we have no resultset + # which is ok (eg UPDATE/INSERT) + if ( + type(e).__name__ == 'OperationalError' and + str(e) == 'Statement not executed or executed statement has no resultset' + ): + query_results.append([]) + else: + error_msg = '%s: %s' % (type(e).__name__, str(e)) + module.fail_json(msg="query failed", query=query, error=error_msg, **result) # ensure that the result is json serializable qry_results = json.loads(json.dumps(query_results, default=clean_output)) diff --git a/tests/integration/targets/mssql_script/tasks/main.yml b/tests/integration/targets/mssql_script/tasks/main.yml index 6fa4d35014..481522216d 100644 --- a/tests/integration/targets/mssql_script/tasks/main.yml +++ b/tests/integration/targets/mssql_script/tasks/main.yml @@ -49,6 +49,19 @@ login_port: "{{ mssql_port }}" script: "SELECT 1" +- name: Execute a malformed query + community.general.mssql_script: + login_user: "{{ mssql_login_user }}" + login_password: "{{ mssql_login_password }}" + login_host: "{{ mssql_host }}" + login_port: "{{ mssql_port }}" + script: "SELCT 1" + failed_when: false + register: bad_query +- assert: + that: + - bad_query.error.startswith('ProgrammingError') + - name: two batches with default output community.general.mssql_script: login_user: "{{ mssql_login_user }}" @@ -135,6 +148,30 @@ - result_batches_dict.query_results_dict[0][0] | length == 1 # one row in first select - result_batches_dict.query_results_dict[0][0][0]['b0s0'] == 'Batch 0 - Select 0' # column 'b0s0' of first row +- name: Multiple batches with no resultsets and mixed-case GO + community.general.mssql_script: + login_user: "{{ mssql_login_user }}" + login_password: "{{ mssql_login_password }}" + login_host: "{{ mssql_host }}" + login_port: "{{ mssql_port }}" + script: | + CREATE TABLE #integration56yH2 (c1 VARCHAR(10), c2 VARCHAR(10)) + Go + INSERT INTO #integration56yH2 VALUES ('C1_VALUE1', 'C2_VALUE1') + gO + UPDATE #integration56yH2 SET c2 = 'C2_VALUE2' WHERE c1 = 'C1_VALUE1' + go + SELECT * from #integration56yH2 + GO + DROP TABLE #integration56yH2 + register: empty_batches +- assert: + that: + - empty_batches.query_results | length == 5 # five batch results + - empty_batches.query_results[3][0] | length == 1 # one row in select + - empty_batches.query_results[3][0][0] | length == 2 # two columns in row + - empty_batches.query_results[3][0][0][1] == 'C2_VALUE2' # value has been updated + - name: Stored procedure may return multiple result sets community.general.mssql_script: login_user: "{{ mssql_login_user }}"