This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 32e1f2e3af3 Add BteqOperator for Executing Teradata BTEQ Scripts in 
Airflow (#51807)
32e1f2e3af3 is described below

commit 32e1f2e3af302231188c774dfb08f26ecd142505
Author: Satish Chinthanippu <[email protected]>
AuthorDate: Sat Jun 21 03:06:11 2025 -0700

    Add BteqOperator for Executing Teradata BTEQ Scripts in Airflow (#51807)
    
    * Add BteqOperator for Executing Teradata BTEQ Scripts in Airflow (#58)
    
    BTEQ (Basic Teradata Query) is a command-line utility provided by Teradata 
for interacting with Teradata databases. The BteqOperator is designed to 
provide flexible and robust execution of Teradata SQL Statements and BTEQ 
script/SQL file using the BTEQ command-line utility within Airflow DAGs
    
    * system test failure fixed
    
    * unit tests issues fixed
    
    * unit tests issues fixed
    
    * As per airflow recommendation, removed usage of Caplog
    
    * airflow workflow issue fixed
    
    * Documentation is improved
    
    ---------
    
    Co-authored-by: Satish Ch <[email protected]>
---
 docs/spelling_wordlist.txt                         |   1 +
 providers/teradata/docs/operators/bteq.rst         | 264 +++++++++++++++
 providers/teradata/provider.yaml                   |   9 +
 providers/teradata/pyproject.toml                  |   4 +
 .../providers/teradata/get_provider_info.py        |   7 +-
 .../src/airflow/providers/teradata/hooks/bteq.py   | 325 ++++++++++++++++++
 .../src/airflow/providers/teradata/hooks/ttu.py    |  93 ++++++
 .../airflow/providers/teradata/operators/bteq.py   | 284 ++++++++++++++++
 .../airflow/providers/teradata/utils/bteq_util.py  | 182 +++++++++++
 .../providers/teradata/utils/encryption_utils.py   |  75 +++++
 .../teradata/tests/system/teradata/example_bteq.py | 272 +++++++++++++++
 .../tests/system/teradata/example_remote_bteq.py   | 272 +++++++++++++++
 providers/teradata/tests/system/teradata/script    |  15 +
 .../teradata/tests/system/teradata/script.bteq     |  15 +
 .../teradata/tests/system/teradata/script.sql      |  29 ++
 .../tests/system/teradata/script_utf16.bteq        | Bin 0 -> 980 bytes
 .../tests/unit/teradata/hooks/test_bteq.py         | 364 +++++++++++++++++++++
 .../teradata/tests/unit/teradata/hooks/test_ttu.py | 136 ++++++++
 .../tests/unit/teradata/operators/test_bteq.py     | 288 ++++++++++++++++
 .../tests/unit/teradata/utils/test_bteq_util.py    | 187 +++++++++++
 .../unit/teradata/utils/test_encryption_utils.py   | 108 ++++++
 21 files changed, 2928 insertions(+), 2 deletions(-)

diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 192f79e1397..36cdf13b293 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -215,6 +215,7 @@ botocore
 BounceX
 Bq
 bq
+bteq
 bugfix
 bugfixes
 buildType
diff --git a/providers/teradata/docs/operators/bteq.rst 
b/providers/teradata/docs/operators/bteq.rst
new file mode 100644
index 00000000000..2424aa9855e
--- /dev/null
+++ b/providers/teradata/docs/operators/bteq.rst
@@ -0,0 +1,264 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+ ..   http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+.. _howto/operator:BteqOperator:
+
+BteqOperator
+============
+
+The :class:`~airflow.providers.teradata.operators.bteq.BteqOperator` enables 
execution of SQL statements or BTEQ (Basic Teradata Query) scripts using the 
Teradata BTEQ utility, which can be installed either locally or accessed 
remotely via SSH.
+
+This is useful for executing administrative operations, batch queries, or ETL 
tasks in Teradata environments using the Teradata BTEQ utility.
+
+.. note::
+
+   This operator requires the Teradata Tools and Utilities (TTU) including the 
``bteq`` binary to be installed
+   and accessible via the system's ``PATH`` (either locally or on the remote 
SSH host).
+
+Use the ``BteqOperator`` when you want to:
+
+- Run parameterized or templated SQL/BTEQ scripts
+- Connect securely to Teradata with Airflow connections
+- Execute queries via SSH on remote systems with BTEQ installed
+
+Prerequisite
+------------
+
+Make sure your Teradata Airflow connection is defined with the required fields:
+
+- ``host``
+- ``login``
+- ``password``
+- Optional: ``database``, etc.
+
+You can define a remote host with a separate SSH connection using the 
``ssh_conn_id``.
+
+.. note::
+
+   For improved security, it is **highly recommended** to use
+   **private key-based SSH authentication** (SSH key pairs) instead of 
username/password
+   for the SSH connection.
+
+   This avoids password exposure, enables seamless automated execution, and 
enhances security.
+
+   See the Airflow SSH Connection documentation for details on configuring SSH 
keys:
+   
https://airflow.apache.org/docs/apache-airflow/stable/howto/connection/ssh.html
+
+
+To execute arbitrary SQL or BTEQ commands in a Teradata database, use the
+:class:`~airflow.providers.teradata.operators.bteq.BteqOperator`.
+
+Common Database Operations with BteqOperator when BTEQ is installed on local 
machine
+-------------------------------------------------------------------------------------
+
+Creating a Teradata database table
+----------------------------------
+
+You can use the BteqOperator to create tables in a Teradata database. The 
following example demonstrates how to create a simple employee table:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_create_table]
+    :end-before: [END bteq_operator_howto_guide_create_table]
+
+The BTEQ script within this operator handles the table creation, including 
defining columns, data types, and constraints.
+
+
+Inserting data into a Teradata database table
+---------------------------------------------
+
+The following example demonstrates how to populate the ``my_employees`` table 
with sample employee records:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_populate_table]
+    :end-before: [END bteq_operator_howto_guide_populate_table]
+
+This BTEQ script inserts multiple rows into the table in a single operation, 
making it efficient for batch data loading.
+
+
+Exporting data from a Teradata database table to a file
+-------------------------------------------------------
+
+The BteqOperator makes it straightforward to export query results to a file. 
This capability is valuable for data extraction, backups, and transferring data 
between systems. The following example demonstrates how to query the employee 
table and export the results:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_export_data_to_a_file]
+    :end-before: [END bteq_operator_howto_guide_export_data_to_a_file]
+
+The BTEQ script above handles the data export with options for formatting, 
file location specification, and error handling during the export process.
+
+
+Fetching and processing records from your Teradata database
+-----------------------------------------------------------
+
+You can use BteqOperator to query and retrieve data from your Teradata tables. 
The following example demonstrates
+how to fetch specific records from the employee table with filtering and 
formatting:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_get_it_employees]
+    :end-before: [END bteq_operator_howto_guide_get_it_employees]
+
+Executing a BTEQ script with the BteqOperator
+---------------------------------------------
+
+You can use BteqOperator to execute a BTEQ script directly. This is useful for 
running complex queries or scripts that require multiple SQL statements or 
specific BTEQ commands.
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_bteq_file_input]
+    :end-before: [END bteq_operator_howto_guide_bteq_file_input]
+
+
+Common Database Operations with BteqOperator when BTEQ is installed on remote 
machine
+-------------------------------------------------------------------------------------
+
+Make sure SSH connection is defined with the required fields to connect to 
remote machine:
+
+- ``remote_host``
+- ``username``
+- ``password``
+- Optional: ``key_file``, ``private_key``, ``conn_timeout``, etc.
+
+Creating a Teradata database table
+----------------------------------
+
+You can use the BteqOperator to create tables in a Teradata database. The 
following example demonstrates how to create a simple employee table:
+
+.. exampleinclude:: 
/../../teradata/tests/system/teradata/example_remote_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_create_table]
+    :end-before: [END bteq_operator_howto_guide_create_table]
+
+The BTEQ script within this operator handles the table creation, including 
defining columns, data types, and constraints.
+
+
+Inserting data into a Teradata database table
+---------------------------------------------
+
+The following example demonstrates how to populate the ``my_employees`` table 
with sample employee records:
+
+.. exampleinclude:: 
/../../teradata/tests/system/teradata/example_remote_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_populate_table]
+    :end-before: [END bteq_operator_howto_guide_populate_table]
+
+This BTEQ script inserts multiple rows into the table in a single operation, 
making it efficient for batch data loading.
+
+
+Exporting data from a Teradata database table to a file
+-------------------------------------------------------
+
+The BteqOperator makes it straightforward to export query results to a file. 
This capability is valuable for data extraction, backups, and transferring data 
between systems. The following example demonstrates how to query the employee 
table and export the results:
+
+.. exampleinclude:: 
/../../teradata/tests/system/teradata/example_remote_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_export_data_to_a_file]
+    :end-before: [END bteq_operator_howto_guide_export_data_to_a_file]
+
+The BTEQ script above handles the data export with options for formatting, 
file location specification, and error handling during the export process.
+
+
+Fetching and processing records from your Teradata database
+-----------------------------------------------------------
+
+You can use BteqOperator to query and retrieve data from your Teradata tables. 
The following example demonstrates
+how to fetch specific records from the employee table with filtering and 
formatting:
+
+.. exampleinclude:: 
/../../teradata/tests/system/teradata/example_remote_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_get_it_employees]
+    :end-before: [END bteq_operator_howto_guide_get_it_employees]
+
+This example shows how to:
+- Execute a SELECT query with WHERE clause filtering
+- Format the output for better readability
+- Process the result set within the BTEQ script
+- Handle empty result sets appropriately
+
+Executing a BTEQ script with the BteqOperator when BTEQ script file is on 
remote machine
+----------------------------------------------------------------------------------------
+
+You can use BteqOperator to execute a BTEQ script directly when file is on 
remote machine.
+
+.. exampleinclude:: 
/../../teradata/tests/system/teradata/example_remote_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_bteq_file_input]
+    :end-before: [END bteq_operator_howto_guide_bteq_file_input]
+
+
+Using Conditional Logic with BteqOperator
+-----------------------------------------
+
+The BteqOperator supports executing conditional logic within your BTEQ 
scripts. This powerful feature lets you create dynamic, decision-based 
workflows that respond to data conditions or processing results:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_conditional_logic]
+    :end-before: [END bteq_operator_howto_guide_conditional_logic]
+
+Conditional execution enables more intelligent data pipelines that can adapt 
to different scenarios without requiring separate DAG branches.
+
+
+Error Handling in BTEQ Scripts
+------------------------------
+
+The BteqOperator allows you to implement comprehensive error handling within 
your BTEQ scripts:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_error_handling]
+    :end-before: [END bteq_operator_howto_guide_error_handling]
+
+This approach lets you catch and respond to errors at the BTEQ script level, 
providing more granular control over error conditions and enabling appropriate 
recovery actions.
+
+
+Dropping a Teradata Database Table
+----------------------------------
+
+When your workflow completes or requires cleanup, you can use the BteqOperator 
to drop database objects. The following example demonstrates how to drop the 
``my_employees`` table:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :dedent: 4
+    :start-after: [START bteq_operator_howto_guide_drop_table]
+    :end-before: [END bteq_operator_howto_guide_drop_table]
+
+
+The complete Teradata Operator DAG
+----------------------------------
+
+When we put everything together, our DAG should look like this:
+
+.. exampleinclude:: /../../teradata/tests/system/teradata/example_bteq.py
+    :language: python
+    :start-after: [START bteq_operator_howto_guide]
+    :end-before: [END bteq_operator_howto_guide]
diff --git a/providers/teradata/provider.yaml b/providers/teradata/provider.yaml
index 153e825ccd7..01563d9d0de 100644
--- a/providers/teradata/provider.yaml
+++ b/providers/teradata/provider.yaml
@@ -57,11 +57,20 @@ operators:
     python-modules:
       - airflow.providers.teradata.operators.teradata
       - airflow.providers.teradata.operators.teradata_compute_cluster
+  - integration-name: Bteq
+    python-modules:
+      - airflow.providers.teradata.operators.bteq
 
 hooks:
   - integration-name: Teradata
     python-modules:
       - airflow.providers.teradata.hooks.teradata
+  - integration-name: Ttu
+    python-modules:
+      - airflow.providers.teradata.hooks.ttu
+  - integration-name: Bteq
+    python-modules:
+      - airflow.providers.teradata.hooks.bteq
 
 transfers:
   - source-integration-name: Teradata
diff --git a/providers/teradata/pyproject.toml 
b/providers/teradata/pyproject.toml
index d2dbb359b30..3866c56d0a6 100644
--- a/providers/teradata/pyproject.toml
+++ b/providers/teradata/pyproject.toml
@@ -72,6 +72,9 @@ dependencies = [
 "amazon" = [
     "apache-airflow-providers-amazon",
 ]
+"ssh" = [
+    "apache-airflow-providers-ssh"
+]
 
 [dependency-groups]
 dev = [
@@ -81,6 +84,7 @@ dev = [
     "apache-airflow-providers-amazon",
     "apache-airflow-providers-common-sql",
     "apache-airflow-providers-microsoft-azure",
+    "apache-airflow-providers-ssh",
     # Additional devel dependencies (do not remove this line and add extra 
development dependencies)
 ]
 
diff --git 
a/providers/teradata/src/airflow/providers/teradata/get_provider_info.py 
b/providers/teradata/src/airflow/providers/teradata/get_provider_info.py
index 7780d600f05..f9b935fa551 100644
--- a/providers/teradata/src/airflow/providers/teradata/get_provider_info.py
+++ b/providers/teradata/src/airflow/providers/teradata/get_provider_info.py
@@ -45,10 +45,13 @@ def get_provider_info():
                     "airflow.providers.teradata.operators.teradata",
                     
"airflow.providers.teradata.operators.teradata_compute_cluster",
                 ],
-            }
+            },
+            {"integration-name": "Bteq", "python-modules": 
["airflow.providers.teradata.operators.bteq"]},
         ],
         "hooks": [
-            {"integration-name": "Teradata", "python-modules": 
["airflow.providers.teradata.hooks.teradata"]}
+            {"integration-name": "Teradata", "python-modules": 
["airflow.providers.teradata.hooks.teradata"]},
+            {"integration-name": "Ttu", "python-modules": 
["airflow.providers.teradata.hooks.ttu"]},
+            {"integration-name": "Bteq", "python-modules": 
["airflow.providers.teradata.hooks.bteq"]},
         ],
         "transfers": [
             {
diff --git a/providers/teradata/src/airflow/providers/teradata/hooks/bteq.py 
b/providers/teradata/src/airflow/providers/teradata/hooks/bteq.py
new file mode 100644
index 00000000000..89aac594ddb
--- /dev/null
+++ b/providers/teradata/src/airflow/providers/teradata/hooks/bteq.py
@@ -0,0 +1,325 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import os
+import socket
+import subprocess
+import tempfile
+from contextlib import contextmanager
+
+from paramiko import SSHException
+
+from airflow.exceptions import AirflowException
+from airflow.providers.ssh.hooks.ssh import SSHHook
+from airflow.providers.teradata.hooks.ttu import TtuHook
+from airflow.providers.teradata.utils.bteq_util import (
+    prepare_bteq_command_for_local_execution,
+    prepare_bteq_command_for_remote_execution,
+    transfer_file_sftp,
+    verify_bteq_installed,
+    verify_bteq_installed_remote,
+)
+from airflow.providers.teradata.utils.encryption_utils import (
+    decrypt_remote_file_to_string,
+    generate_encrypted_file_with_openssl,
+    generate_random_password,
+)
+
+
+class BteqHook(TtuHook):
+    """
+    Hook for executing BTEQ (Basic Teradata Query) scripts.
+
+    This hook provides functionality to execute BTEQ scripts either locally or 
remotely via SSH.
+    It extends the `TtuHook` and integrates with Airflow's SSHHook for remote 
execution.
+
+    The BTEQ scripts are used to interact with Teradata databases, allowing 
users to perform
+    operations such as querying, data manipulation, and administrative tasks.
+
+    Features:
+    - Supports both local and remote execution of BTEQ scripts.
+    - Handles connection details, script preparation, and execution.
+    - Provides robust error handling and logging for debugging.
+    - Allows configuration of session parameters like output width and 
encoding.
+
+    .. seealso::
+        - :ref:`hook API connection <howto/connection:teradata>`
+
+    :param bteq_script: The BTEQ script to be executed. This can be a string 
containing the BTEQ commands.
+    :param remote_working_dir: Temporary directory location on the remote host 
(via SSH) where the BTEQ script will be transferred and executed. Defaults to 
`/tmp` if not specified. This is only applicable when `ssh_conn_id` is provided.
+    :param bteq_script_encoding: Character encoding for the BTEQ script file. 
Defaults to ASCII if not specified.
+    :param timeout: Timeout (in seconds) for executing the BTEQ command. 
Default is 600 seconds (10 minutes).
+    :param timeout_rc: Return code to use if the BTEQ execution fails due to a 
timeout. To allow DAG execution to continue after a timeout, include this value 
in `bteq_quit_rc`. If not specified, a timeout will raise an exception and stop 
the DAG.
+    :param bteq_session_encoding: Character encoding for the BTEQ session. 
Defaults to UTF-8 if not specified.
+    :param bteq_quit_rc: Accepts a single integer, list, or tuple of return 
codes. Specifies which BTEQ return codes should be treated as successful, 
allowing subsequent tasks to continue execution.
+    """
+
+    def __init__(self, teradata_conn_id: str, ssh_conn_id: str | None = None, 
*args, **kwargs):
+        super().__init__(teradata_conn_id, *args, **kwargs)
+        self.ssh_conn_id = ssh_conn_id
+        self.ssh_hook = SSHHook(ssh_conn_id=ssh_conn_id) if ssh_conn_id else 
None
+
+    def execute_bteq_script(
+        self,
+        bteq_script: str,
+        remote_working_dir: str | None,
+        bteq_script_encoding: str | None,
+        timeout: int,
+        timeout_rc: int | None,
+        bteq_session_encoding: str | None,
+        bteq_quit_rc: int | list[int] | tuple[int, ...] | None,
+        temp_file_read_encoding: str | None,
+    ) -> int | None:
+        """Execute the BTEQ script either in local machine or on remote host 
based on ssh_conn_id."""
+        # Remote execution
+        if self.ssh_hook:
+            # Write script to local temp file
+            # Encrypt the file locally
+            return self.execute_bteq_script_at_remote(
+                bteq_script,
+                remote_working_dir,
+                bteq_script_encoding,
+                timeout,
+                timeout_rc,
+                bteq_session_encoding,
+                bteq_quit_rc,
+                temp_file_read_encoding,
+            )
+        return self.execute_bteq_script_at_local(
+            bteq_script,
+            bteq_script_encoding,
+            timeout,
+            timeout_rc,
+            bteq_quit_rc,
+            bteq_session_encoding,
+            temp_file_read_encoding,
+        )
+
+    def execute_bteq_script_at_remote(
+        self,
+        bteq_script: str,
+        remote_working_dir: str | None,
+        bteq_script_encoding: str | None,
+        timeout: int,
+        timeout_rc: int | None,
+        bteq_session_encoding: str | None,
+        bteq_quit_rc: int | list[int] | tuple[int, ...] | None,
+        temp_file_read_encoding: str | None,
+    ) -> int | None:
+        with (
+            self.preferred_temp_directory() as tmp_dir,
+        ):
+            file_path = os.path.join(tmp_dir, "bteq_script.txt")
+            with open(file_path, "w", encoding=str(temp_file_read_encoding or 
"UTF-8")) as f:
+                f.write(bteq_script)
+            return self._transfer_to_and_execute_bteq_on_remote(
+                file_path,
+                remote_working_dir,
+                bteq_script_encoding,
+                timeout,
+                timeout_rc,
+                bteq_quit_rc,
+                bteq_session_encoding,
+                tmp_dir,
+            )
+
+    def _transfer_to_and_execute_bteq_on_remote(
+        self,
+        file_path: str,
+        remote_working_dir: str | None,
+        bteq_script_encoding: str | None,
+        timeout: int,
+        timeout_rc: int | None,
+        bteq_quit_rc: int | list[int] | tuple[int, ...] | None,
+        bteq_session_encoding: str | None,
+        tmp_dir: str,
+    ) -> int | None:
+        encrypted_file_path = None
+        remote_encrypted_path = None
+        try:
+            if self.ssh_hook and self.ssh_hook.get_conn():
+                with self.ssh_hook.get_conn() as ssh_client:
+                    if ssh_client is None:
+                        raise AirflowException("Failed to establish SSH 
connection. `ssh_client` is None.")
+                    verify_bteq_installed_remote(ssh_client)
+                    password = generate_random_password()  # 
Encryption/Decryption password
+                    encrypted_file_path = os.path.join(tmp_dir, 
"bteq_script.enc")
+                    generate_encrypted_file_with_openssl(file_path, password, 
encrypted_file_path)
+                    remote_encrypted_path = os.path.join(remote_working_dir or 
"", "bteq_script.enc")
+
+                    transfer_file_sftp(ssh_client, encrypted_file_path, 
remote_encrypted_path)
+
+                    bteq_command_str = 
prepare_bteq_command_for_remote_execution(
+                        timeout=timeout,
+                        bteq_script_encoding=bteq_script_encoding or "",
+                        bteq_session_encoding=bteq_session_encoding or "",
+                        timeout_rc=timeout_rc or -1,
+                    )
+
+                    exit_status, stdout, stderr = 
decrypt_remote_file_to_string(
+                        ssh_client,
+                        remote_encrypted_path,
+                        password,
+                        bteq_command_str,
+                    )
+
+                    failure_message = None
+                    password = None  # Clear sensitive data
+
+                    if "Failure" in stderr or "Error" in stderr:
+                        failure_message = stderr
+                    # Raising an exception if there is any failure in bteq and 
also user wants to fail the
+                    # task otherwise just log the error message as warning to 
not fail the task.
+                    if (
+                        failure_message
+                        and exit_status != 0
+                        and exit_status
+                        not in (
+                            bteq_quit_rc
+                            if isinstance(bteq_quit_rc, (list, tuple))
+                            else [bteq_quit_rc if bteq_quit_rc is not None 
else 0]
+                        )
+                    ):
+                        raise AirflowException(f"BTEQ task failed with error: 
{failure_message}")
+                    if failure_message:
+                        self.log.warning(failure_message)
+                    return exit_status
+            else:
+                raise AirflowException("SSH connection is not established. 
`ssh_hook` is None or invalid.")
+        except (OSError, socket.gaierror):
+            raise AirflowException(
+                "SSH connection timed out. Please check the network or server 
availability."
+            )
+        except SSHException as e:
+            raise AirflowException(f"An unexpected error occurred during SSH 
connection: {str(e)}")
+        except AirflowException as e:
+            raise e
+        except Exception as e:
+            raise AirflowException(
+                f"An unexpected error occurred while executing BTEQ script on 
remote machine: {str(e)}"
+            )
+        finally:
+            # Remove the local script file
+            if encrypted_file_path and os.path.exists(encrypted_file_path):
+                os.remove(encrypted_file_path)
+            # Cleanup: Delete the remote temporary file
+            if encrypted_file_path:
+                cleanup_en_command = f"rm -f {remote_encrypted_path}"
+                if self.ssh_hook and self.ssh_hook.get_conn():
+                    with self.ssh_hook.get_conn() as ssh_client:
+                        if ssh_client is None:
+                            raise AirflowException(
+                                "Failed to establish SSH connection. 
`ssh_client` is None."
+                            )
+                        ssh_client.exec_command(cleanup_en_command)
+
+    def execute_bteq_script_at_local(
+        self,
+        bteq_script: str,
+        bteq_script_encoding: str | None,
+        timeout: int,
+        timeout_rc: int | None,
+        bteq_quit_rc: int | list[int] | tuple[int, ...] | None,
+        bteq_session_encoding: str | None,
+        temp_file_read_encoding: str | None,
+    ) -> int | None:
+        verify_bteq_installed()
+        bteq_command_str = prepare_bteq_command_for_local_execution(
+            self.get_conn(),
+            timeout=timeout,
+            bteq_script_encoding=bteq_script_encoding or "",
+            bteq_session_encoding=bteq_session_encoding or "",
+            timeout_rc=timeout_rc or -1,
+        )
+        process = subprocess.Popen(
+            bteq_command_str,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT,
+            shell=True,
+            preexec_fn=os.setsid,
+        )
+        encode_bteq_script = bteq_script.encode(str(temp_file_read_encoding or 
"UTF-8"))
+        stdout_data, _ = process.communicate(input=encode_bteq_script)
+        try:
+            # 
https://docs.python.org/3.10/library/subprocess.html#subprocess.Popen.wait  
timeout is in seconds
+            process.wait(timeout=timeout + 60)  # Adding 1 minute extra for 
BTEQ script timeout
+        except subprocess.TimeoutExpired:
+            self.on_kill()
+            raise AirflowException(f"BTEQ command timed out after {timeout} 
seconds.")
+        conn = self.get_conn()
+        conn["sp"] = process  # For `on_kill` support
+        failure_message = None
+        if stdout_data is None:
+            raise AirflowException("Process stdout is None. Unable to read 
BTEQ output.")
+        decoded_line = ""
+        for line in stdout_data.splitlines():
+            try:
+                decoded_line = line.decode("UTF-8").strip()
+            except UnicodeDecodeError:
+                self.log.warning("Failed to decode line: %s", line)
+            if "Failure" in decoded_line or "Error" in decoded_line:
+                failure_message = decoded_line
+        # Raising an exception if there is any failure in bteq and also user 
wants to fail the
+        # task otherwise just log the error message as warning to not fail the 
task.
+        if (
+            failure_message
+            and process.returncode != 0
+            and process.returncode
+            not in (
+                bteq_quit_rc
+                if isinstance(bteq_quit_rc, (list, tuple))
+                else [bteq_quit_rc if bteq_quit_rc is not None else 0]
+            )
+        ):
+            raise AirflowException(f"BTEQ task failed with error: 
{failure_message}")
+        if failure_message:
+            self.log.warning(failure_message)
+
+        return process.returncode
+
+    def on_kill(self):
+        """Terminate the subprocess if running."""
+        conn = self.get_conn()
+        process = conn.get("sp")
+        if process:
+            try:
+                process.terminate()
+                process.wait(timeout=5)
+            except subprocess.TimeoutExpired:
+                self.log.warning("Subprocess did not terminate in time. 
Forcing kill...")
+                process.kill()
+            except Exception as e:
+                self.log.error("Failed to terminate subprocess: %s", str(e))
+
+    def get_airflow_home_dir(self) -> str:
+        """Get the AIRFLOW_HOME directory."""
+        return os.environ.get("AIRFLOW_HOME", "~/airflow")
+
+    @contextmanager
+    def preferred_temp_directory(self, prefix="bteq_"):
+        try:
+            temp_dir = tempfile.gettempdir()
+            if not os.path.isdir(temp_dir) or not os.access(temp_dir, os.W_OK):
+                raise OSError("OS temp dir not usable")
+        except Exception:
+            temp_dir = self.get_airflow_home_dir()
+
+        with tempfile.TemporaryDirectory(dir=temp_dir, prefix=prefix) as tmp:
+            yield tmp
diff --git a/providers/teradata/src/airflow/providers/teradata/hooks/ttu.py 
b/providers/teradata/src/airflow/providers/teradata/hooks/ttu.py
new file mode 100644
index 00000000000..958bd76796e
--- /dev/null
+++ b/providers/teradata/src/airflow/providers/teradata/hooks/ttu.py
@@ -0,0 +1,93 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import subprocess
+from abc import ABC
+from typing import Any
+
+from airflow.exceptions import AirflowException
+from airflow.hooks.base import BaseHook
+
+
+class TtuHook(BaseHook, ABC):
+    """
+    Abstract base hook for integrating Teradata Tools and Utilities (TTU) in 
Airflow.
+
+    This hook provides common connection handling, resource management, and 
lifecycle
+    support for TTU based operations such as BTEQ, TLOAD, and TPT.
+
+    It should not be used directly. Instead, it must be subclassed by concrete 
hooks
+    like `BteqHook`, `TloadHook`, or `TddlHook` that implement the actual TTU 
command logic.
+
+    Core Features:
+    - Establishes a reusable Teradata connection configuration.
+    - Provides context management for safe resource cleanup.
+    - Manages subprocess termination (e.g., for long-running TTU jobs).
+
+    Requirements:
+    - TTU command-line tools must be installed and accessible via PATH.
+    - A valid Airflow connection with Teradata credentials must be configured.
+    """
+
+    def __init__(self, teradata_conn_id: str = "teradata_default", *args, 
**kwargs) -> None:
+        super().__init__()
+        self.teradata_conn_id = teradata_conn_id
+        self.conn: dict[str, Any] | None = None
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+        if self.conn is not None:
+            self.close_conn()
+
+    def get_conn(self) -> dict[str, Any]:
+        """
+        Set up and return a Teradata connection dictionary.
+
+        This dictionary includes connection credentials and a subprocess 
placeholder.
+        Ensures connection is created only once per hook instance.
+
+        :return: Dictionary with connection details.
+        """
+        if not self.conn:
+            connection = self.get_connection(self.teradata_conn_id)
+            if not connection.login or not connection.password or not 
connection.host:
+                raise AirflowException("Missing required connection 
parameters: login, password, or host.")
+
+            self.conn = dict(
+                login=connection.login,
+                password=connection.password,
+                host=connection.host,
+                database=connection.schema,
+                sp=None,  # Subprocess placeholder
+            )
+        return self.conn
+
+    def close_conn(self):
+        """Terminate any active TTU subprocess and clear the connection."""
+        if self.conn:
+            if self.conn.get("sp") and self.conn["sp"].poll() is None:
+                self.conn["sp"].terminate()
+                try:
+                    self.conn["sp"].wait(timeout=5)
+                except subprocess.TimeoutExpired:
+                    self.log.warning("Subprocess did not terminate in time. 
Forcing kill...")
+                    self.conn["sp"].kill()
+            self.conn = None
diff --git 
a/providers/teradata/src/airflow/providers/teradata/operators/bteq.py 
b/providers/teradata/src/airflow/providers/teradata/operators/bteq.py
new file mode 100644
index 00000000000..22779be9bfb
--- /dev/null
+++ b/providers/teradata/src/airflow/providers/teradata/operators/bteq.py
@@ -0,0 +1,284 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Literal
+
+from airflow.providers.teradata.utils.bteq_util import (
+    is_valid_encoding,
+    is_valid_file,
+    is_valid_remote_bteq_script_file,
+    prepare_bteq_script_for_local_execution,
+    prepare_bteq_script_for_remote_execution,
+    read_file,
+)
+
+if TYPE_CHECKING:
+    from paramiko import SSHClient
+
+    try:
+        from airflow.sdk.definitions.context import Context
+    except ImportError:
+        from airflow.utils.context import Context
+
+from airflow.models import BaseOperator
+from airflow.providers.ssh.hooks.ssh import SSHHook
+from airflow.providers.teradata.hooks.bteq import BteqHook
+from airflow.providers.teradata.hooks.teradata import TeradataHook
+
+
+def contains_template(parameter_value):
+    # Check if the parameter contains Jinja templating syntax
+    return "{{" in parameter_value and "}}" in parameter_value
+
+
+class BteqOperator(BaseOperator):
+    """
+    Teradata Operator to execute SQL Statements or BTEQ (Basic Teradata Query) 
scripts using Teradata BTEQ utility.
+
+    This supports execution of BTEQ scripts either locally or remotely via SSH.
+
+    The BTEQ scripts are used to interact with Teradata databases, allowing 
users to perform
+    operations such as querying, data manipulation, and administrative tasks.
+
+    Features:
+    - Supports both local and remote execution of BTEQ scripts.
+    - Handles connection details, script preparation, and execution.
+    - Provides robust error handling and logging for debugging.
+    - Allows configuration of session parameters like session and BTEQ I/O 
encoding.
+
+    .. seealso::
+        For more information on how to use this operator, take a look at the 
guide:
+        :ref:`howto/operator:BteqOperator`
+
+    :param sql: SQL statement(s) to be executed using BTEQ. (templated)
+    :param file_path: Optional path to an existing SQL or BTEQ script file. If 
provided, this file will be used instead of the `sql` content. This path 
represents remote file path when executing remotely via SSH, or local file path 
when executing locally.
+    :param teradata_conn_id: Reference to a specific Teradata connection.
+    :param ssh_conn_id: Optional SSH connection ID for remote execution. Used 
only when executing scripts remotely.
+    :param remote_working_dir: Temporary directory location on the remote host 
(via SSH) where the BTEQ script will be transferred and executed. Defaults to 
`/tmp` if not specified. This is only applicable when `ssh_conn_id` is provided.
+    :param bteq_session_encoding: Character set encoding for the BTEQ session. 
Defaults to ASCII if not specified.
+    :param bteq_script_encoding: Character encoding for the BTEQ script file. 
Defaults to ASCII if not specified.
+    :param bteq_quit_rc: Accepts a single integer, list, or tuple of return 
codes. Specifies which BTEQ return codes should be treated as successful, 
allowing subsequent tasks to continue execution.
+    :param timeout: Timeout (in seconds) for executing the BTEQ command. 
Default is 600 seconds (10 minutes).
+    :param timeout_rc: Return code to use if the BTEQ execution fails due to a 
timeout. To allow DAG execution to continue after a timeout, include this value 
in `bteq_quit_rc`. If not specified, a timeout will raise an exception and stop 
the DAG.
+    """
+
+    template_fields = "sql"
+    ui_color = "#ff976d"
+
+    def __init__(
+        self,
+        *,
+        sql: str | None = None,
+        file_path: str | None = None,
+        teradata_conn_id: str = TeradataHook.default_conn_name,
+        ssh_conn_id: str | None = None,
+        remote_working_dir: str | None = None,
+        bteq_session_encoding: str | None = None,
+        bteq_script_encoding: str | None = None,
+        bteq_quit_rc: int | list[int] | tuple[int, ...] | None = None,
+        timeout: int | Literal[600] = 600,  # Default to 10 minutes
+        timeout_rc: int | None = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.sql = sql
+        self.file_path = file_path
+        self.teradata_conn_id = teradata_conn_id
+        self.ssh_conn_id = ssh_conn_id
+        self.remote_working_dir = remote_working_dir
+        self.timeout = timeout
+        self.timeout_rc = timeout_rc
+        self.bteq_session_encoding = bteq_session_encoding
+        self.bteq_script_encoding = bteq_script_encoding
+        self.bteq_quit_rc = bteq_quit_rc
+        self._hook: BteqHook | None = None
+        self._ssh_hook: SSHHook | None = None
+        self.temp_file_read_encoding = "UTF-8"
+
+    def execute(self, context: Context) -> int | None:
+        """Execute BTEQ code using the BteqHook."""
+        if not self.sql and not self.file_path:
+            raise ValueError(
+                "BteqOperator requires either the 'sql' or 'file_path' 
parameter. Both are missing."
+            )
+        self._hook = BteqHook(teradata_conn_id=self.teradata_conn_id, 
ssh_conn_id=self.ssh_conn_id)
+        self._ssh_hook = SSHHook(ssh_conn_id=self.ssh_conn_id) if 
self.ssh_conn_id else None
+
+        # Validate and set BTEQ session and script encoding
+        if not self.bteq_session_encoding or self.bteq_session_encoding == 
"ASCII":
+            self.bteq_session_encoding = ""
+            if self.bteq_script_encoding == "UTF8":
+                self.temp_file_read_encoding = "UTF-8"
+            elif self.bteq_script_encoding == "UTF16":
+                self.temp_file_read_encoding = "UTF-16"
+            self.bteq_script_encoding = ""
+        elif self.bteq_session_encoding == "UTF8" and (
+            not self.bteq_script_encoding or self.bteq_script_encoding == 
"ASCII"
+        ):
+            self.bteq_script_encoding = "UTF8"
+        elif self.bteq_session_encoding == "UTF16":
+            if not self.bteq_script_encoding or self.bteq_script_encoding == 
"ASCII":
+                self.bteq_script_encoding = "UTF8"
+        # for file reading in python. Mapping BTEQ encoding to Python encoding
+        if self.bteq_script_encoding == "UTF8":
+            self.temp_file_read_encoding = "UTF-8"
+        elif self.bteq_script_encoding == "UTF16":
+            self.temp_file_read_encoding = "UTF-16"
+
+        if not self.remote_working_dir:
+            self.remote_working_dir = "/tmp"
+        # Handling execution on local:
+        if not self._ssh_hook:
+            if self.sql:
+                bteq_script = prepare_bteq_script_for_local_execution(
+                    sql=self.sql,
+                )
+                return self._hook.execute_bteq_script(
+                    bteq_script,
+                    self.remote_working_dir,
+                    self.bteq_script_encoding,
+                    self.timeout,
+                    self.timeout_rc,
+                    self.bteq_session_encoding,
+                    self.bteq_quit_rc,
+                    self.temp_file_read_encoding,
+                )
+            if self.file_path:
+                if not is_valid_file(self.file_path):
+                    raise ValueError(
+                        f"The provided file path '{self.file_path}' is invalid 
or does not exist."
+                    )
+                try:
+                    is_valid_encoding(self.file_path, 
self.temp_file_read_encoding or "UTF-8")
+                except UnicodeDecodeError as e:
+                    errmsg = f"The provided file '{self.file_path}' encoding 
is different from BTEQ I/O encoding i.e.'UTF-8'."
+                    if self.bteq_script_encoding:
+                        errmsg = f"The provided file '{self.file_path}' 
encoding is different from the specified BTEQ I/O encoding 
'{self.bteq_script_encoding}'."
+                    raise ValueError(errmsg) from e
+                return self._handle_local_bteq_file(
+                    file_path=self.file_path,
+                    context=context,
+                )
+        # Execution on Remote machine
+        elif self._ssh_hook:
+            # When sql statement is provided as input through sql parameter, 
Preparing the bteq script
+            if self.sql:
+                bteq_script = prepare_bteq_script_for_remote_execution(
+                    conn=self._hook.get_conn(),
+                    sql=self.sql,
+                )
+                return self._hook.execute_bteq_script(
+                    bteq_script,
+                    self.remote_working_dir,
+                    self.bteq_script_encoding,
+                    self.timeout,
+                    self.timeout_rc,
+                    self.bteq_session_encoding,
+                    self.bteq_quit_rc,
+                    self.temp_file_read_encoding,
+                )
+            if self.file_path:
+                with self._ssh_hook.get_conn() as ssh_client:
+                    # When .sql or .bteq remote file path is provided as input 
through file_path parameter, executing on remote machine
+                    if self.file_path and 
is_valid_remote_bteq_script_file(ssh_client, self.file_path):
+                        return self._handle_remote_bteq_file(
+                            ssh_client=self._ssh_hook.get_conn(),
+                            file_path=self.file_path,
+                            context=context,
+                        )
+                    raise ValueError(
+                        f"The provided remote file path '{self.file_path}' is 
invalid or file does not exist on remote machine at given path."
+                    )
+            else:
+                raise ValueError(
+                    "BteqOperator requires either the 'sql' or 'file_path' 
parameter. Both are missing."
+                )
+        return None
+
+    def _handle_remote_bteq_file(
+        self,
+        ssh_client: SSHClient,
+        file_path: str | None,
+        context: Context,
+    ) -> int | None:
+        if file_path:
+            with ssh_client:
+                sftp = ssh_client.open_sftp()
+                try:
+                    with sftp.open(file_path, "r") as remote_file:
+                        original_content = 
remote_file.read().decode(self.temp_file_read_encoding or "UTF-8")
+                finally:
+                    sftp.close()
+                rendered_content = original_content
+                if contains_template(original_content):
+                    rendered_content = self.render_template(original_content, 
context)
+                if self._hook:
+                    bteq_script = prepare_bteq_script_for_remote_execution(
+                        conn=self._hook.get_conn(),
+                        sql=rendered_content,
+                    )
+                    return self._hook.execute_bteq_script_at_remote(
+                        bteq_script,
+                        self.remote_working_dir,
+                        self.bteq_script_encoding,
+                        self.timeout,
+                        self.timeout_rc,
+                        self.bteq_session_encoding,
+                        self.bteq_quit_rc,
+                        self.temp_file_read_encoding,
+                    )
+            return None
+        raise ValueError(
+            "Please provide a valid file path for the BTEQ script to be 
executed on the remote machine."
+        )
+
+    def _handle_local_bteq_file(
+        self,
+        file_path: str,
+        context: Context,
+    ) -> int | None:
+        if file_path and is_valid_file(file_path):
+            file_content = read_file(file_path, 
encoding=str(self.temp_file_read_encoding or "UTF-8"))
+            # Manually render using operator's context
+            rendered_content = file_content
+            if contains_template(file_content):
+                rendered_content = self.render_template(file_content, context)
+            bteq_script = prepare_bteq_script_for_local_execution(
+                sql=rendered_content,
+            )
+            if self._hook:
+                result = self._hook.execute_bteq_script(
+                    bteq_script,
+                    self.remote_working_dir,
+                    self.bteq_script_encoding,
+                    self.timeout,
+                    self.timeout_rc,
+                    self.bteq_session_encoding,
+                    self.bteq_quit_rc,
+                    self.temp_file_read_encoding,
+                )
+                return result
+        return None
+
+    def on_kill(self) -> None:
+        """Handle task termination by invoking the on_kill method of 
BteqHook."""
+        if self._hook:
+            self._hook.on_kill()
+        else:
+            self.log.warning("BteqHook was not initialized. Nothing to 
terminate.")
diff --git 
a/providers/teradata/src/airflow/providers/teradata/utils/bteq_util.py 
b/providers/teradata/src/airflow/providers/teradata/utils/bteq_util.py
new file mode 100644
index 00000000000..0741ebb2009
--- /dev/null
+++ b/providers/teradata/src/airflow/providers/teradata/utils/bteq_util.py
@@ -0,0 +1,182 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import shutil
+import stat
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+    from paramiko import SSHClient
+
+from airflow.exceptions import AirflowException
+
+
+def verify_bteq_installed():
+    """Verify if BTEQ is installed and available in the system's PATH."""
+    if shutil.which("bteq") is None:
+        raise AirflowException("BTEQ is not installed or not available in the 
system's PATH.")
+
+
+def verify_bteq_installed_remote(ssh_client: SSHClient):
+    """Verify if BTEQ is installed on the remote machine."""
+    stdin, stdout, stderr = ssh_client.exec_command("which bteq")
+    exit_status = stdout.channel.recv_exit_status()
+    output = stdout.read().strip()
+    error = stderr.read().strip()
+
+    if exit_status != 0 or not output:
+        raise AirflowException(
+            f"BTEQ is not installed or not available in PATH. stderr: 
{error.decode() if error else 'N/A'}"
+        )
+
+
+def transfer_file_sftp(ssh_client, local_path, remote_path):
+    sftp = ssh_client.open_sftp()
+    sftp.put(local_path, remote_path)
+    sftp.close()
+
+
+# We can not pass host details with bteq command when executing on remote 
machine. Instead, we will prepare .logon in bteq script itself to avoid risk of
+# exposing sensitive information
+def prepare_bteq_script_for_remote_execution(conn: dict[str, Any], sql: str) 
-> str:
+    """Build a BTEQ script with necessary connection and session commands."""
+    script_lines = []
+    host = conn["host"]
+    login = conn["login"]
+    password = conn["password"]
+    script_lines.append(f" .LOGON {host}/{login},{password}")
+    return _prepare_bteq_script(script_lines, sql)
+
+
+def prepare_bteq_script_for_local_execution(
+    sql: str,
+) -> str:
+    """Build a BTEQ script with necessary connection and session commands."""
+    script_lines: list[str] = []
+    return _prepare_bteq_script(script_lines, sql)
+
+
+def _prepare_bteq_script(script_lines: list[str], sql: str) -> str:
+    script_lines.append(sql.strip())
+    script_lines.append(".EXIT")
+    return "\n".join(script_lines)
+
+
+def _prepare_bteq_command(
+    timeout: int,
+    bteq_script_encoding: str,
+    bteq_session_encoding: str,
+    timeout_rc: int,
+) -> list[str]:
+    bteq_core_cmd = ["bteq"]
+    if bteq_session_encoding:
+        bteq_core_cmd.append(f" -e {bteq_script_encoding}")
+        bteq_core_cmd.append(f" -c {bteq_session_encoding}")
+    bteq_core_cmd.append('"')
+    bteq_core_cmd.append(f".SET EXITONDELAY ON MAXREQTIME {timeout}")
+    if timeout_rc is not None and timeout_rc >= 0:
+        bteq_core_cmd.append(f" RC {timeout_rc}")
+    bteq_core_cmd.append(";")
+    # Airflow doesn't display the script of BTEQ in UI but only in log so 
WIDTH is 500 enough
+    bteq_core_cmd.append(" .SET WIDTH 500;")
+    return bteq_core_cmd
+
+
+def prepare_bteq_command_for_remote_execution(
+    timeout: int,
+    bteq_script_encoding: str,
+    bteq_session_encoding: str,
+    timeout_rc: int,
+) -> str:
+    """Prepare the BTEQ command with necessary parameters."""
+    bteq_core_cmd = _prepare_bteq_command(timeout, bteq_script_encoding, 
bteq_session_encoding, timeout_rc)
+    bteq_core_cmd.append('"')
+    return " ".join(bteq_core_cmd)
+
+
+def prepare_bteq_command_for_local_execution(
+    conn: dict[str, Any],
+    timeout: int,
+    bteq_script_encoding: str,
+    bteq_session_encoding: str,
+    timeout_rc: int,
+) -> str:
+    """Prepare the BTEQ command with necessary parameters."""
+    bteq_core_cmd = _prepare_bteq_command(timeout, bteq_script_encoding, 
bteq_session_encoding, timeout_rc)
+    host = conn["host"]
+    login = conn["login"]
+    password = conn["password"]
+    bteq_core_cmd.append(f" .LOGON {host}/{login},{password}")
+    bteq_core_cmd.append('"')
+    bteq_command_str = " ".join(bteq_core_cmd)
+    return bteq_command_str
+
+
+def is_valid_file(file_path: str) -> bool:
+    return os.path.isfile(file_path)
+
+
+def is_valid_encoding(file_path: str, encoding: str = "UTF-8") -> bool:
+    """
+    Check if the file can be read with the specified encoding.
+
+    :param file_path: Path to the file to be checked.
+    :param encoding: Encoding to use for reading the file.
+    :return: True if the file can be read with the specified encoding, False 
otherwise.
+    """
+    with open(file_path, encoding=encoding) as f:
+        f.read()
+        return True
+
+
+def read_file(file_path: str, encoding: str = "UTF-8") -> str:
+    """
+    Read the content of a file with the specified encoding.
+
+    :param file_path: Path to the file to be read.
+    :param encoding: Encoding to use for reading the file.
+    :return: Content of the file as a string.
+    """
+    if not os.path.isfile(file_path):
+        raise FileNotFoundError(f"The file {file_path} does not exist.")
+
+    with open(file_path, encoding=encoding) as f:
+        return f.read()
+
+
+def is_valid_remote_bteq_script_file(ssh_client: SSHClient, remote_file_path: 
str, logger=None) -> bool:
+    """Check if the given remote file path is a valid BTEQ script file."""
+    if remote_file_path:
+        sftp_client = ssh_client.open_sftp()
+        try:
+            # Get file metadata
+            file_stat = sftp_client.stat(remote_file_path)
+            if file_stat.st_mode:
+                is_regular_file = stat.S_ISREG(file_stat.st_mode)
+                return is_regular_file
+            return False
+        except FileNotFoundError:
+            if logger:
+                logger.error("File does not exist on remote at : %s", 
remote_file_path)
+            return False
+        finally:
+            sftp_client.close()
+    else:
+        return False
diff --git 
a/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py 
b/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py
new file mode 100644
index 00000000000..57ed4b98558
--- /dev/null
+++ 
b/providers/teradata/src/airflow/providers/teradata/utils/encryption_utils.py
@@ -0,0 +1,75 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import secrets
+import string
+import subprocess
+
+
+def generate_random_password(length=12):
+    # Define the character set: letters, digits, and special characters
+    characters = string.ascii_letters + string.digits + string.punctuation
+    # Generate a random password
+    password = "".join(secrets.choice(characters) for _ in range(length))
+    return password
+
+
+def generate_encrypted_file_with_openssl(file_path: str, password: str, 
out_file: str):
+    # Write plaintext temporarily to file
+
+    # Run openssl enc with AES-256-CBC, pbkdf2, salt
+    cmd = [
+        "openssl",
+        "enc",
+        "-aes-256-cbc",
+        "-salt",
+        "-pbkdf2",
+        "-pass",
+        f"pass:{password}",
+        "-in",
+        file_path,
+        "-out",
+        out_file,
+    ]
+    subprocess.run(cmd, check=True)
+
+
+def decrypt_remote_file_to_string(ssh_client, remote_enc_file, password, 
bteq_command_str):
+    # Run openssl decrypt command on remote machine
+    quoted_password = shell_quote_single(password)
+
+    decrypt_cmd = (
+        f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass 
pass:{quoted_password} -in {remote_enc_file} | "
+        + bteq_command_str
+    )
+    # Clear password to prevent lingering sensitive data
+    password = None
+    quoted_password = None
+    stdin, stdout, stderr = ssh_client.exec_command(decrypt_cmd)
+    # Wait for command to finish
+    exit_status = stdout.channel.recv_exit_status()
+    output = stdout.read().decode()
+    err = stderr.read().decode()
+    return exit_status, output, err
+
+
+def shell_quote_single(s):
+    # Escape single quotes in s, then wrap in single quotes
+    # In shell, to include a single quote inside single quotes, close, add 
'\'' and reopen
+    return "'" + s.replace("'", "'\\''") + "'"
diff --git a/providers/teradata/tests/system/teradata/example_bteq.py 
b/providers/teradata/tests/system/teradata/example_bteq.py
new file mode 100644
index 00000000000..a77f81bc697
--- /dev/null
+++ b/providers/teradata/tests/system/teradata/example_bteq.py
@@ -0,0 +1,272 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Example Airflow DAG to show usage of BteqOperator.
+
+This DAG assumes Airflow Connection with connection id `TTU_DEFAULT` already 
exists in locally. It
+shows how to use Teradata BTEQ commands with BteqOperator as tasks in
+airflow dags using BteqeOperator.
+"""
+
+from __future__ import annotations
+
+import datetime
+import os
+
+import pytest
+
+from airflow import DAG
+
+try:
+    from airflow.providers.teradata.operators.bteq import BteqOperator
+except ImportError:
+    pytest.skip("TERADATA provider not available", allow_module_level=True)
+
+# [START bteq_operator_howto_guide]
+
+
+ENV_ID = os.environ.get("SYSTEM_TESTS_ENV_ID")
+DAG_ID = "example_bteq"
+CONN_ID = "teradata_default"
+SSH_CONN_ID = "ssh_default"
+
+host = os.environ.get("host", "localhost")
+username = os.environ.get("username", "temp")
+password = os.environ.get("password", "temp")
+params = {
+    "host": host,
+    "username": username,
+    "password": password,
+    "DATABASE_NAME": "airflow",
+    "TABLE_NAME": "my_employees",
+    "DB_TABLE_NAME": "airflow.my_employees",
+}
+with DAG(
+    dag_id=DAG_ID,
+    start_date=datetime.datetime(2020, 2, 2),
+    schedule="@once",
+    catchup=False,
+    default_args={"teradata_conn_id": CONN_ID, "params": params},
+) as dag:
+    # [START bteq_operator_howto_guide_create_table]
+    create_table = BteqOperator(
+        task_id="create_table",
+        sql=r"""
+                CREATE SET TABLE {{params.DB_TABLE_NAME}} (
+                  emp_id INT,
+                  emp_name VARCHAR(100),
+                  dept VARCHAR(50)
+                ) PRIMARY INDEX (emp_id);
+                """,
+        bteq_quit_rc=[0, 4],
+        timeout=20,
+        bteq_session_encoding="UTF8",
+        bteq_script_encoding="UTF8",
+        params=params,
+    )
+    # [END bteq_operator_howto_guide_create_table]
+    # [START bteq_operator_howto_guide_populate_table]
+    populate_table = BteqOperator(
+        task_id="populate_table",
+        sql=r"""
+                INSERT INTO {{params.DB_TABLE_NAME}} VALUES (1, 'John Doe', 
'IT');
+                INSERT INTO {{params.DB_TABLE_NAME}} VALUES (2, 'Jane Smith', 
'HR');
+                """,
+        params=params,
+        bteq_session_encoding="UTF8",
+        bteq_quit_rc=0,
+    )
+    # [END bteq_operator_howto_guide_populate_table]
+
+    # [START bteq_operator_howto_guide_export_data_to_a_file]
+    export_to_a_file = BteqOperator(
+        task_id="export_to_a_file",
+        sql=r"""
+                .EXPORT FILE = employees_output.txt;
+                SELECT * FROM {{params.DB_TABLE_NAME}};
+                .EXPORT RESET;
+                """,
+        bteq_session_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_export_data_to_a_file]
+
+    # [START bteq_operator_howto_guide_get_it_employees]
+    get_it_employees = BteqOperator(
+        task_id="get_it_employees",
+        sql=r"""
+                SELECT * FROM {{params.DB_TABLE_NAME}} WHERE dept = 'IT';
+                """,
+        bteq_session_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_get_it_employees]
+
+    # [START bteq_operator_howto_guide_conditional_logic]
+    cond_logic = BteqOperator(
+        task_id="cond_logic",
+        sql=r"""
+                .IF ERRORCODE <> 0 THEN .GOTO handle_error;
+
+                SELECT COUNT(*) FROM {{params.DB_TABLE_NAME}};
+
+                .LABEL handle_error;
+                """,
+        bteq_script_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_conditional_logic]
+
+    # [START bteq_operator_howto_guide_error_handling]
+    error_handling = BteqOperator(
+        task_id="error_handling",
+        sql=r"""
+                DROP TABLE my_temp;
+                .IF ERRORCODE = 3807 THEN .GOTO table_not_found;
+                SELECT 'Table dropped successfully.';
+                .GOTO end;
+
+                .LABEL table_not_found;
+                SELECT 'Table not found - continuing execution';
+                .LABEL end;
+                .LOGOFF;
+                .QUIT 0;
+                """,
+        bteq_script_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_error_handling]
+
+    # [START bteq_operator_howto_guide_drop_table]
+    drop_table = BteqOperator(
+        task_id="drop_table",
+        sql=r"""
+                DROP TABLE {{params.DB_TABLE_NAME}};
+                .IF ERRORCODE = 3807 THEN .GOTO end;
+
+                .LABEL end;
+                .LOGOFF;
+                .QUIT 0;
+                """,
+        bteq_script_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_drop_table]
+    # [START bteq_operator_howto_guide_bteq_file_input]
+    execute_bteq_file = BteqOperator(
+        task_id="execute_bteq_file",
+        file_path="providers/teradata/tests/system/teradata/script.bteq",
+        params=params,
+    )
+    # [END bteq_operator_howto_guide_bteq_file_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_input]
+    execute_bteq_utf8_file = BteqOperator(
+        task_id="execute_bteq_utf8_file",
+        file_path="providers/teradata/tests/system/teradata/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_session_ascii_input]
+    execute_bteq_utf8_session_ascii_file = BteqOperator(
+        task_id="execute_bteq_utf8_session_ascii_file",
+        file_path="providers/teradata/tests/system/teradata/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+        bteq_session_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_session_ascii_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_session_utf8_input]
+    execute_bteq_utf8_session_utf8_file = BteqOperator(
+        task_id="execute_bteq_utf8_session_utf8_file",
+        file_path="providers/teradata/tests/system/teradata/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+        bteq_session_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_session_utf8_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_session_utf16_input]
+    execute_bteq_utf8_session_utf16_file = BteqOperator(
+        task_id="execute_bteq_utf8_session_utf16_file",
+        file_path="providers/teradata/tests/system/teradata/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+        bteq_session_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_session_utf16_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_input]
+    execute_bteq_utf16_file = BteqOperator(
+        task_id="execute_bteq_utf16_file",
+        file_path="providers/teradata/tests/system/teradata/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_input]
+    execute_bteq_utf16_session_ascii_file = BteqOperator(
+        task_id="execute_bteq_utf16_session_ascii_file",
+        file_path="providers/teradata/tests/system/teradata/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+        bteq_session_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    execute_bteq_utf16_session_utf8_file = BteqOperator(
+        task_id="execute_bteq_utf16_session_utf8_file",
+        file_path="providers/teradata/tests/system/teradata/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+        bteq_session_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    execute_bteq_utf16_session_utf16_file = BteqOperator(
+        task_id="execute_bteq_utf16_session_utf16_file",
+        file_path="providers/teradata/tests/system/teradata/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+        bteq_session_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    (
+        create_table
+        >> populate_table
+        >> export_to_a_file
+        >> get_it_employees
+        >> cond_logic
+        >> error_handling
+        >> drop_table
+        >> execute_bteq_file
+        >> execute_bteq_utf8_file
+        >> execute_bteq_utf8_session_ascii_file
+        >> execute_bteq_utf8_session_utf8_file
+        >> execute_bteq_utf8_session_utf16_file
+        >> execute_bteq_utf16_file
+        >> execute_bteq_utf16_session_ascii_file
+        >> execute_bteq_utf16_session_utf8_file
+        >> execute_bteq_utf16_session_utf16_file
+    )
+
+    # [END bteq_operator_howto_guide]
+
+    from tests_common.test_utils.watcher import watcher
+
+    # This test needs watcher in order to properly mark success/failure
+    # when "tearDown" task with trigger rule is part of the DAG
+    list(dag.tasks) >> watcher()
+
+from tests_common.test_utils.system_tests import get_test_run  # noqa: E402
+
+# Needed to run the example DAG with pytest (see: 
tests/system/README.md#run_via_pytest)
+test_run = get_test_run(dag)
diff --git a/providers/teradata/tests/system/teradata/example_remote_bteq.py 
b/providers/teradata/tests/system/teradata/example_remote_bteq.py
new file mode 100644
index 00000000000..76bedff0b95
--- /dev/null
+++ b/providers/teradata/tests/system/teradata/example_remote_bteq.py
@@ -0,0 +1,272 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Example Airflow DAG to show usage of BteqOperator.
+
+This DAG assumes Airflow Connection with connection id `TTU_DEFAULT` already 
exists in locally. It
+shows how to use Teradata BTEQ commands with BteqOperator as tasks in
+airflow dags using BteqeOperator.
+"""
+
+from __future__ import annotations
+
+import datetime
+import os
+
+import pytest
+
+from airflow import DAG
+
+try:
+    from airflow.providers.teradata.operators.bteq import BteqOperator
+except ImportError:
+    pytest.skip("TERADATA provider not available", allow_module_level=True)
+
+# [START bteq_operator_howto_guide]
+
+
+ENV_ID = os.environ.get("SYSTEM_TESTS_ENV_ID")
+DAG_ID = "example_remote_bteq"
+CONN_ID = "teradata_default"
+SSH_CONN_ID = "ssh_default"
+
+host = os.environ.get("host", "localhost")
+username = os.environ.get("username", "temp")
+password = os.environ.get("password", "temp")
+params = {
+    "host": host,
+    "username": username,
+    "password": password,
+    "DATABASE_NAME": "airflow",
+    "TABLE_NAME": "my_employees",
+    "DB_TABLE_NAME": "airflow.my_employees",
+}
+with DAG(
+    dag_id=DAG_ID,
+    start_date=datetime.datetime(2020, 2, 2),
+    schedule="@once",
+    catchup=False,
+    default_args={"teradata_conn_id": CONN_ID, "params": params, 
"ssh_conn_id": SSH_CONN_ID},
+) as dag:
+    # [START bteq_operator_howto_guide_create_table]
+    create_table = BteqOperator(
+        task_id="create_table",
+        sql=r"""
+                CREATE SET TABLE {{params.DB_TABLE_NAME}} (
+                  emp_id INT,
+                  emp_name VARCHAR(100),
+                  dept VARCHAR(50)
+                ) PRIMARY INDEX (emp_id);
+                """,
+        bteq_quit_rc=[0, 4],
+        timeout=20,
+        bteq_session_encoding="UTF8",
+        bteq_script_encoding="UTF8",
+        params=params,
+    )
+    # [END bteq_operator_howto_guide_create_table]
+    # [START bteq_operator_howto_guide_populate_table]
+    populate_table = BteqOperator(
+        task_id="populate_table",
+        sql=r"""
+                INSERT INTO {{params.DB_TABLE_NAME}} VALUES (1, 'John Doe', 
'IT');
+                INSERT INTO {{params.DB_TABLE_NAME}} VALUES (2, 'Jane Smith', 
'HR');
+                """,
+        params=params,
+        bteq_session_encoding="UTF8",
+        bteq_quit_rc=0,
+    )
+    # [END bteq_operator_howto_guide_populate_table]
+
+    # [START bteq_operator_howto_guide_export_data_to_a_file]
+    export_to_a_file = BteqOperator(
+        task_id="export_to_a_file",
+        sql=r"""
+                .EXPORT FILE = employees_output.txt;
+                SELECT * FROM {{params.DB_TABLE_NAME}};
+                .EXPORT RESET;
+                """,
+        bteq_session_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_export_data_to_a_file]
+
+    # [START bteq_operator_howto_guide_get_it_employees]
+    get_it_employees = BteqOperator(
+        task_id="get_it_employees",
+        sql=r"""
+                SELECT * FROM {{params.DB_TABLE_NAME}} WHERE dept = 'IT';
+                """,
+        bteq_session_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_get_it_employees]
+
+    # [START bteq_operator_howto_guide_conditional_logic]
+    cond_logic = BteqOperator(
+        task_id="cond_logic",
+        sql=r"""
+                .IF ERRORCODE <> 0 THEN .GOTO handle_error;
+
+                SELECT COUNT(*) FROM {{params.DB_TABLE_NAME}};
+
+                .LABEL handle_error;
+                """,
+        bteq_script_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_conditional_logic]
+
+    # [START bteq_operator_howto_guide_error_handling]
+    error_handling = BteqOperator(
+        task_id="error_handling",
+        sql=r"""
+                DROP TABLE my_temp;
+                .IF ERRORCODE = 3807 THEN .GOTO table_not_found;
+                SELECT 'Table dropped successfully.';
+                .GOTO end;
+
+                .LABEL table_not_found;
+                SELECT 'Table not found - continuing execution';
+                .LABEL end;
+                .LOGOFF;
+                .QUIT 0;
+                """,
+        bteq_script_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_error_handling]
+
+    # [START bteq_operator_howto_guide_drop_table]
+    drop_table = BteqOperator(
+        task_id="drop_table",
+        sql=r"""
+                DROP TABLE {{params.DB_TABLE_NAME}};
+                .IF ERRORCODE = 3807 THEN .GOTO end;
+
+                .LABEL end;
+                .LOGOFF;
+                .QUIT 0;
+                """,
+        bteq_script_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_drop_table]
+    # [START bteq_operator_howto_guide_bteq_file_input]
+    execute_bteq_file = BteqOperator(
+        task_id="execute_bteq_file",
+        file_path="/home/devtools/satish/airflow/script.bteq",
+        params=params,
+    )
+    # [END bteq_operator_howto_guide_bteq_file_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_input]
+    execute_bteq_utf8_file = BteqOperator(
+        task_id="execute_bteq_utf8_file",
+        file_path="/home/devtools/satish/airflow/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_session_ascii_input]
+    execute_bteq_utf8_session_ascii_file = BteqOperator(
+        task_id="execute_bteq_utf8_session_ascii_file",
+        file_path="/home/devtools/satish/airflow/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+        bteq_session_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_session_ascii_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_session_utf8_input]
+    execute_bteq_utf8_session_utf8_file = BteqOperator(
+        task_id="execute_bteq_utf8_session_utf8_file",
+        file_path="/home/devtools/satish/airflow/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+        bteq_session_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_session_utf8_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf8_session_utf16_input]
+    execute_bteq_utf8_session_utf16_file = BteqOperator(
+        task_id="execute_bteq_utf8_session_utf16_file",
+        file_path="/home/devtools/satish/airflow/script.bteq",
+        params=params,
+        bteq_script_encoding="UTF8",
+        bteq_session_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf8_session_utf16_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_input]
+    execute_bteq_utf16_file = BteqOperator(
+        task_id="execute_bteq_utf16_file",
+        file_path="/home/devtools/satish/airflow/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_input]
+    execute_bteq_utf16_session_ascii_file = BteqOperator(
+        task_id="execute_bteq_utf16_session_ascii_file",
+        file_path="/home/devtools/satish/airflow/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+        bteq_session_encoding="ASCII",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    execute_bteq_utf16_session_utf8_file = BteqOperator(
+        task_id="execute_bteq_utf16_session_utf8_file",
+        file_path="/home/devtools/satish/airflow/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+        bteq_session_encoding="UTF8",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    # [START bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    execute_bteq_utf16_session_utf16_file = BteqOperator(
+        task_id="execute_bteq_utf16_session_utf16_file",
+        file_path="/home/devtools/satish/airflow/script_utf16.bteq",
+        params=params,
+        bteq_script_encoding="UTF16",
+        bteq_session_encoding="UTF16",
+    )
+    # [END bteq_operator_howto_guide_bteq_file_utf16_session_utf8_input]
+    (
+        create_table
+        >> populate_table
+        >> export_to_a_file
+        >> get_it_employees
+        >> cond_logic
+        >> error_handling
+        >> drop_table
+        >> execute_bteq_file
+        >> execute_bteq_utf8_file
+        >> execute_bteq_utf8_session_ascii_file
+        >> execute_bteq_utf8_session_utf8_file
+        >> execute_bteq_utf8_session_utf16_file
+        >> execute_bteq_utf16_file
+        >> execute_bteq_utf16_session_ascii_file
+        >> execute_bteq_utf16_session_utf8_file
+        >> execute_bteq_utf16_session_utf16_file
+    )
+
+    # [END bteq_operator_howto_guide]
+
+    from tests_common.test_utils.watcher import watcher
+
+    # This test needs watcher in order to properly mark success/failure
+    # when "tearDown" task with trigger rule is part of the DAG
+    list(dag.tasks) >> watcher()
+
+from tests_common.test_utils.system_tests import get_test_run  # noqa: E402
+
+# Needed to run the example DAG with pytest (see: 
tests/system/README.md#run_via_pytest)
+test_run = get_test_run(dag)
diff --git a/providers/teradata/tests/system/teradata/script 
b/providers/teradata/tests/system/teradata/script
new file mode 100644
index 00000000000..346e647b1ae
--- /dev/null
+++ b/providers/teradata/tests/system/teradata/script
@@ -0,0 +1,15 @@
+.LOGON {{params.host}}/{{params.username}},{{params.password}}
+.IF ERRORCODE <> 0 THEN .QUIT 8
+.SET WIDTH 500
+.SET SESSION CHARSET 'ASCII'
+DATABASE {{params.DATABASE_NAME}};
+CREATE SET TABLE {{params.TABLE_NAME}} (
+    emp_id INT,
+    emp_name VARCHAR(100),
+    dept VARCHAR(50)
+    ) PRIMARY INDEX (emp_id);
+INSERT INTO {{params.TABLE_NAME}} VALUES (1, 'John Doe', 'IT');
+INSERT INTO {{params.TABLE_NAME}} VALUES (2, 'Jane Smith', 'HR');
+DROP TABLE {{params.TABLE_NAME}};
+.LOGOFF
+.quit 0
diff --git a/providers/teradata/tests/system/teradata/script.bteq 
b/providers/teradata/tests/system/teradata/script.bteq
new file mode 100644
index 00000000000..346e647b1ae
--- /dev/null
+++ b/providers/teradata/tests/system/teradata/script.bteq
@@ -0,0 +1,15 @@
+.LOGON {{params.host}}/{{params.username}},{{params.password}}
+.IF ERRORCODE <> 0 THEN .QUIT 8
+.SET WIDTH 500
+.SET SESSION CHARSET 'ASCII'
+DATABASE {{params.DATABASE_NAME}};
+CREATE SET TABLE {{params.TABLE_NAME}} (
+    emp_id INT,
+    emp_name VARCHAR(100),
+    dept VARCHAR(50)
+    ) PRIMARY INDEX (emp_id);
+INSERT INTO {{params.TABLE_NAME}} VALUES (1, 'John Doe', 'IT');
+INSERT INTO {{params.TABLE_NAME}} VALUES (2, 'Jane Smith', 'HR');
+DROP TABLE {{params.TABLE_NAME}};
+.LOGOFF
+.quit 0
diff --git a/providers/teradata/tests/system/teradata/script.sql 
b/providers/teradata/tests/system/teradata/script.sql
new file mode 100644
index 00000000000..84d93c5fe9a
--- /dev/null
+++ b/providers/teradata/tests/system/teradata/script.sql
@@ -0,0 +1,29 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+DATABASE {{params.DATABASE_NAME}};
+CREATE SET TABLE {{params.TABLE_NAME}} (
+          emp_id INT,
+          emp_name VARCHAR(100),
+          dept VARCHAR(50)
+        ) PRIMARY INDEX (emp_id);
+INSERT INTO {{params.TABLE_NAME}} VALUES (1, 'John Doe', 'IT');
+INSERT INTO {{params.TABLE_NAME}} VALUES (2, 'Jane Smith', 'HR');
+SELECT * FROM {{params.TABLE_NAME}};
+DROP TABLE {{params.TABLE_NAME}};
diff --git a/providers/teradata/tests/system/teradata/script_utf16.bteq 
b/providers/teradata/tests/system/teradata/script_utf16.bteq
new file mode 100644
index 00000000000..34d329deb91
Binary files /dev/null and 
b/providers/teradata/tests/system/teradata/script_utf16.bteq differ
diff --git a/providers/teradata/tests/unit/teradata/hooks/test_bteq.py 
b/providers/teradata/tests/unit/teradata/hooks/test_bteq.py
new file mode 100644
index 00000000000..46b50e652b5
--- /dev/null
+++ b/providers/teradata/tests/unit/teradata/hooks/test_bteq.py
@@ -0,0 +1,364 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import subprocess
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from airflow.exceptions import AirflowException
+from airflow.providers.teradata.hooks.bteq import BteqHook
+
+
[email protected]
+def dummy_bteq_script():
+    return "SELECT * FROM dbc.tables;"
+
+
[email protected]
+def dummy_remote_dir():
+    return "/tmp"
+
+
[email protected]
+def dummy_encoding():
+    return "utf-8"
+
+
[email protected]
+def dummy_password():
+    return "dummy_password"
+
+
[email protected]
+def hook_without_ssh():
+    return BteqHook(ssh_conn_id=None, teradata_conn_id="teradata_conn")
+
+
+@patch("airflow.providers.teradata.hooks.bteq.SSHHook")
+def test_init_sets_ssh_hook(mock_ssh_hook_class):
+    mock_ssh_instance = MagicMock()
+    mock_ssh_hook_class.return_value = mock_ssh_instance
+
+    hook = BteqHook(ssh_conn_id="ssh_conn_id", 
teradata_conn_id="teradata_conn")
+
+    # Validate the call and assignment
+    mock_ssh_hook_class.assert_called_once_with(ssh_conn_id="ssh_conn_id")
+    assert hook.ssh_hook == mock_ssh_instance
+
+
+@patch("subprocess.Popen")
[email protected](
+    BteqHook,
+    "get_conn",
+    return_value={
+        "host": "localhost",
+        "login": "user",
+        "password": "pass",
+        "sp": None,
+    },
+)
+@patch("airflow.providers.teradata.utils.bteq_util.verify_bteq_installed")
+@patch("airflow.providers.teradata.utils.bteq_util.prepare_bteq_command_for_local_execution")
+def test_execute_bteq_script_at_local_timeout(
+    mock_prepare_cmd,
+    mock_verify_bteq,
+    mock_get_conn,
+    mock_popen,
+):
+    hook = BteqHook(ssh_conn_id=None, teradata_conn_id="teradata_conn")
+
+    # Create mock process with timeout simulation
+    mock_process = MagicMock()
+    mock_process.communicate.return_value = (b"some output", None)
+    mock_process.wait.side_effect = 
subprocess.TimeoutExpired(cmd="bteq_command", timeout=5)
+    mock_process.returncode = None
+    mock_popen.return_value = mock_process
+    mock_prepare_cmd.return_value = "bteq_command"
+
+    with pytest.raises(AirflowException):
+        hook.execute_bteq_script_at_local(
+            bteq_script="SELECT * FROM test;",
+            bteq_script_encoding="utf-8",
+            timeout=5,
+            timeout_rc=None,
+            bteq_quit_rc=0,
+            bteq_session_encoding=None,
+            temp_file_read_encoding=None,
+        )
+
+
+@patch("subprocess.Popen")
[email protected](
+    BteqHook,
+    "get_conn",
+    return_value={
+        "host": "localhost",
+        "login": "user",
+        "password": "pass",
+        "sp": None,
+    },
+)
+@patch("airflow.providers.teradata.hooks.bteq.verify_bteq_installed")  # <- 
patch here
+@patch("airflow.providers.teradata.hooks.bteq.prepare_bteq_command_for_local_execution")
  # <- patch here too
+def test_execute_bteq_script_at_local_success(
+    mock_prepare_cmd,
+    mock_verify_bteq,
+    mock_get_conn,
+    mock_popen,
+):
+    hook = BteqHook(teradata_conn_id="teradata_conn")
+
+    mock_process = MagicMock()
+    mock_process.communicate.return_value = (b"Output line 1\nOutput line 
2\n", None)
+    mock_process.wait.return_value = 0
+    mock_process.returncode = 0
+    mock_popen.return_value = mock_process
+    mock_prepare_cmd.return_value = "bteq_command"
+
+    ret_code = hook.execute_bteq_script_at_local(
+        bteq_script="SELECT * FROM test;",
+        bteq_script_encoding="utf-8",
+        timeout=10,
+        timeout_rc=None,
+        bteq_quit_rc=0,
+        bteq_session_encoding=None,
+        temp_file_read_encoding=None,
+    )
+
+    mock_verify_bteq.assert_called_once()
+    mock_prepare_cmd.assert_called_once()
+    mock_popen.assert_called_once_with(
+        "bteq_command",
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        shell=True,
+        preexec_fn=os.setsid,
+    )
+    assert ret_code == 0
+
+
+@patch("subprocess.Popen")
[email protected](
+    BteqHook,
+    "get_conn",
+    return_value={
+        "host": "localhost",
+        "login": "user",
+        "password": "pass",
+        "sp": None,
+    },
+)
+@patch("airflow.providers.teradata.hooks.bteq.verify_bteq_installed")
+@patch("airflow.providers.teradata.hooks.bteq.prepare_bteq_command_for_local_execution")
+def test_execute_bteq_script_at_local_failure_raises(
+    mock_prepare_cmd,
+    mock_verify_bteq,
+    mock_get_conn,
+    mock_popen,
+):
+    hook = BteqHook(ssh_conn_id=None, teradata_conn_id="teradata_conn")
+
+    failure_message = "Failure: some error occurred"
+
+    mock_process = MagicMock()
+    # The output contains "Failure"
+    mock_process.communicate.return_value = (failure_message.encode("utf-8"), 
None)
+    mock_process.wait.return_value = 1
+    mock_process.returncode = 1
+    mock_popen.return_value = mock_process
+    mock_prepare_cmd.return_value = "bteq_command"
+
+    with pytest.raises(AirflowException, match="BTEQ task failed with error: 
Failure: some error occurred"):
+        hook.execute_bteq_script_at_local(
+            bteq_script="SELECT * FROM test;",
+            bteq_script_encoding="utf-8",
+            timeout=10,
+            timeout_rc=None,
+            bteq_quit_rc=0,  # 1 is not allowed here
+            bteq_session_encoding=None,
+            temp_file_read_encoding=None,
+        )
+
+
[email protected](autouse=False)
+def patch_ssh_hook_class():
+    # Patch SSHHook where bteq.py imports it
+    with patch("airflow.providers.teradata.hooks.bteq.SSHHook") as 
mock_ssh_hook_class:
+        mock_ssh_instance = MagicMock()
+        mock_ssh_hook_class.return_value = mock_ssh_instance
+        yield mock_ssh_hook_class
+
+
[email protected]
+def hook_with_ssh(patch_ssh_hook_class):
+    # Now the BteqHook() call will use the patched SSHHook
+    return BteqHook(ssh_conn_id="ssh_conn_id", 
teradata_conn_id="teradata_conn")
+
+
+@patch("airflow.providers.teradata.hooks.bteq.SSHHook")
+@patch("airflow.providers.teradata.hooks.bteq.verify_bteq_installed_remote")
+@patch("airflow.providers.teradata.hooks.bteq.generate_random_password", 
return_value="test_password")
+@patch("airflow.providers.teradata.hooks.bteq.generate_encrypted_file_with_openssl")
+@patch("airflow.providers.teradata.hooks.bteq.transfer_file_sftp")
+@patch(
+    
"airflow.providers.teradata.hooks.bteq.prepare_bteq_command_for_remote_execution",
+    return_value="bteq_command",
+)
+@patch(
+    "airflow.providers.teradata.hooks.bteq.decrypt_remote_file_to_string", 
return_value=(0, ["output"], [])
+)
+def test_execute_bteq_script_at_remote_success(
+    mock_decrypt,
+    mock_prepare_cmd,
+    mock_transfer,
+    mock_encrypt,
+    mock_password,
+    mock_verify,
+    mock_ssh_hook_class,
+):
+    # Mock SSHHook instance and its get_conn() context manager
+    mock_ssh_hook = MagicMock()
+    mock_ssh_client = MagicMock()
+    mock_ssh_hook.get_conn.return_value.__enter__.return_value = 
mock_ssh_client
+    mock_ssh_hook_class.return_value = mock_ssh_hook
+
+    # Instantiate BteqHook with ssh_conn_id (will use mocked SSHHook)
+    hook = BteqHook(ssh_conn_id="ssh_conn_id", 
teradata_conn_id="teradata_conn")
+
+    # Call method under test
+    ret_code = hook.execute_bteq_script_at_remote(
+        bteq_script="SELECT 1;",
+        remote_working_dir="/tmp",
+        bteq_script_encoding="utf-8",
+        timeout=10,
+        timeout_rc=None,
+        bteq_session_encoding="utf-8",
+        bteq_quit_rc=0,
+        temp_file_read_encoding=None,
+    )
+
+    # Assert mocks called as expected
+    mock_verify.assert_called_once_with(mock_ssh_client)
+    mock_password.assert_called_once()
+    mock_encrypt.assert_called_once()
+    mock_transfer.assert_called_once()
+    mock_prepare_cmd.assert_called_once()
+    mock_decrypt.assert_called_once()
+
+    # Assert the return code is what decrypt_remote_file_to_string returns (0 
here)
+    assert ret_code == 0
+
+
+def test_on_kill_terminates_process(hook_without_ssh):
+    process_mock = MagicMock()
+    # Patch the hook's get_conn method to return a dict with the mocked process
+    with patch.object(hook_without_ssh, "get_conn", return_value={"sp": 
process_mock}):
+        hook_without_ssh.on_kill()
+
+        process_mock.terminate.assert_called_once()
+        process_mock.wait.assert_called_once()
+
+
+def test_on_kill_no_process(hook_without_ssh):
+    # Mock get_connection to avoid AirflowNotFoundException
+    with patch.object(hook_without_ssh, "get_connection", 
return_value={"host": "dummy_host"}):
+        # Provide a dummy conn dict to avoid errors
+        with patch.object(hook_without_ssh, "get_conn", return_value={"sp": 
None}):
+            # This should not raise any exceptions even if sp (process) is None
+            hook_without_ssh.on_kill()
+
+
+@patch("airflow.providers.teradata.hooks.bteq.verify_bteq_installed_remote")
+def test_transfer_to_and_execute_bteq_on_remote_ssh_failure(mock_verify, 
hook_with_ssh):
+    # Patch get_conn to simulate SSH failure by returning None
+    hook_with_ssh.ssh_hook.get_conn = MagicMock(return_value=None)
+
+    # Patch helper functions used in the tested function to avoid side effects
+    with (
+        
patch("airflow.providers.teradata.hooks.bteq.generate_random_password", 
return_value="password"),
+        
patch("airflow.providers.teradata.hooks.bteq.generate_encrypted_file_with_openssl"),
+        patch("airflow.providers.teradata.hooks.bteq.transfer_file_sftp"),
+        patch(
+            
"airflow.providers.teradata.hooks.bteq.prepare_bteq_command_for_remote_execution",
+            return_value="cmd",
+        ),
+        patch(
+            
"airflow.providers.teradata.hooks.bteq.decrypt_remote_file_to_string", 
return_value=(0, [], [])
+        ),
+    ):
+        with pytest.raises(AirflowException) as excinfo:
+            hook_with_ssh._transfer_to_and_execute_bteq_on_remote(
+                file_path="/tmp/fakefile",
+                remote_working_dir="/tmp",
+                bteq_script_encoding="utf-8",
+                timeout=10,
+                timeout_rc=None,
+                bteq_quit_rc=0,
+                bteq_session_encoding="utf-8",
+                tmp_dir="/tmp",
+            )
+        assert "SSH connection is not established" in str(excinfo.value)
+
+
+@patch("airflow.providers.teradata.hooks.bteq.verify_bteq_installed_remote")
+@patch("airflow.providers.teradata.hooks.bteq.generate_random_password", 
return_value="testpass")
+@patch("airflow.providers.teradata.hooks.bteq.generate_encrypted_file_with_openssl")
+@patch("airflow.providers.teradata.hooks.bteq.transfer_file_sftp")
+@patch(
+    
"airflow.providers.teradata.hooks.bteq.prepare_bteq_command_for_remote_execution",
+    return_value="bteq_remote_command",
+)
+@patch(
+    "airflow.providers.teradata.hooks.bteq.decrypt_remote_file_to_string",
+    side_effect=Exception("mocked exception"),
+)
+def test_remote_execution_cleanup_on_exception(
+    mock_decrypt,
+    mock_prepare,
+    mock_transfer,
+    mock_generate_enc,
+    mock_generate_pass,
+    mock_verify_remote,
+    hook_with_ssh,
+):
+    temp_dir = "/tmp"
+    local_file_path = os.path.join(temp_dir, "bteq_script.txt")
+    remote_working_dir = temp_dir
+
+    # Make sure the local encrypted file exists for cleanup
+    encrypted_file_path = os.path.join(temp_dir, "bteq_script.enc")
+    with open(encrypted_file_path, "w") as f:
+        f.write("dummy")
+
+    with pytest.raises(AirflowException):
+        hook_with_ssh._transfer_to_and_execute_bteq_on_remote(
+            file_path=local_file_path,
+            remote_working_dir=remote_working_dir,
+            bteq_script_encoding="utf-8",
+            timeout=5,
+            timeout_rc=None,
+            bteq_quit_rc=0,
+            bteq_session_encoding="utf-8",
+            tmp_dir=temp_dir,
+        )
+
+    # After exception, encrypted file should be deleted
+    assert not os.path.exists(encrypted_file_path)
diff --git a/providers/teradata/tests/unit/teradata/hooks/test_ttu.py 
b/providers/teradata/tests/unit/teradata/hooks/test_ttu.py
new file mode 100644
index 00000000000..2c49cb74018
--- /dev/null
+++ b/providers/teradata/tests/unit/teradata/hooks/test_ttu.py
@@ -0,0 +1,136 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import subprocess
+from unittest import mock
+
+import pytest
+
+from airflow.exceptions import AirflowException
+from airflow.providers.teradata.hooks.ttu import TtuHook
+
+
+class TestTtuHook:
+    @mock.patch("airflow.providers.teradata.hooks.ttu.TtuHook.get_connection")
+    def test_get_conn_with_valid_params(self, mock_get_connection):
+        # Setup
+        mock_conn = mock.MagicMock()
+        mock_conn.login = "test_user"
+        mock_conn.password = "test_pass"
+        mock_conn.host = "test_host"
+        mock_conn.extra_dejson = {}
+        mock_get_connection.return_value = mock_conn
+
+        # Execute
+        hook = TtuHook()
+        conn = hook.get_conn()
+
+        # Assert
+        assert conn["login"] == "test_user"
+        assert conn["password"] == "test_pass"
+        assert conn["host"] == "test_host"
+
+    @mock.patch("airflow.providers.teradata.hooks.ttu.TtuHook.get_connection")
+    def test_get_conn_missing_params(self, mock_get_connection):
+        # Setup
+        mock_conn = mock.MagicMock()
+        mock_conn.login = None
+        mock_conn.password = "test_pass"
+        mock_conn.host = "test_host"
+        mock_conn.extra_dejson = {}
+        mock_get_connection.return_value = mock_conn
+
+        # Execute and Assert
+        hook = TtuHook()
+        with pytest.raises(AirflowException, match="Missing required 
connection parameters"):
+            hook.get_conn()
+
+    @mock.patch("subprocess.Popen")
+    @mock.patch("airflow.providers.teradata.hooks.ttu.TtuHook.get_connection")
+    def test_close_conn_subprocess_running(self, mock_get_connection, 
mock_popen):
+        # Setup
+        mock_conn = mock.MagicMock()
+        mock_conn.login = "test_user"
+        mock_conn.password = "test_pass"
+        mock_conn.host = "test_host"
+        mock_conn.extra_dejson = {}
+        mock_get_connection.return_value = mock_conn
+
+        mock_process = mock.MagicMock()
+        mock_process.poll.return_value = None
+        mock_popen.return_value = mock_process
+
+        # Execute
+        hook = TtuHook()
+        conn = hook.get_conn()
+        conn["sp"] = mock_process
+        hook.close_conn()
+
+        # Assert
+        mock_process.terminate.assert_called_once()
+        mock_process.wait.assert_called_once_with(timeout=5)
+        assert hook.conn is None
+
+    @mock.patch("subprocess.Popen")
+    @mock.patch("airflow.providers.teradata.hooks.ttu.TtuHook.get_connection")
+    def test_close_conn_subprocess_timeout(self, mock_get_connection, 
mock_popen):
+        # Setup
+        mock_conn = mock.MagicMock()
+        mock_conn.login = "test_user"
+        mock_conn.password = "test_pass"
+        mock_conn.host = "test_host"
+        mock_conn.extra_dejson = {}
+        mock_get_connection.return_value = mock_conn
+
+        mock_process = mock.MagicMock()
+        mock_process.poll.return_value = None
+        mock_process.wait.side_effect = subprocess.TimeoutExpired(cmd="test", 
timeout=5)
+        mock_popen.return_value = mock_process
+
+        # Execute
+        hook = TtuHook()
+        conn = hook.get_conn()
+        conn["sp"] = mock_process
+        hook.close_conn()
+
+        # Assert
+        mock_process.terminate.assert_called_once()
+        mock_process.wait.assert_called_once()
+        mock_process.kill.assert_called_once()
+        assert hook.conn is None
+
+    @mock.patch("airflow.providers.teradata.hooks.ttu.TtuHook.__exit__")
+    @mock.patch("airflow.providers.teradata.hooks.ttu.TtuHook.__enter__")
+    def test_hook_context_manager(self, mock_enter, mock_exit):
+        # Setup
+        hook = TtuHook()
+        mock_enter.return_value = hook
+
+        # Execute
+        with hook as h:
+            assert h == hook
+
+        # Assert
+        mock_exit.assert_called_once()
+        # Ensure the exit method was called with the correct parameters
+        # Context manager's __exit__ is called with (exc_type, exc_val, exc_tb)
+        args = mock_exit.call_args[0]
+        assert len(args) == 3  # Verify we have the correct number of arguments
+        assert args[0] is None  # type should be None
+        assert args[1] is None  # value should be None
+        assert args[2] is None  # traceback should be None
diff --git a/providers/teradata/tests/unit/teradata/operators/test_bteq.py 
b/providers/teradata/tests/unit/teradata/operators/test_bteq.py
new file mode 100644
index 00000000000..d6690096aa8
--- /dev/null
+++ b/providers/teradata/tests/unit/teradata/operators/test_bteq.py
@@ -0,0 +1,288 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import logging
+import tempfile
+import unittest
+from unittest import mock
+
+import pytest
+
+from airflow.providers.teradata.hooks.bteq import BteqHook
+from airflow.providers.teradata.operators.bteq import BteqOperator
+
+log = logging.getLogger(__name__)
+
+
+class TestBteqOperator:
+    @mock.patch.object(BteqHook, "execute_bteq_script")
+    @mock.patch.object(BteqHook, "__init__", return_value=None)
+    def test_execute(self, mock_hook_init, mock_execute_bteq):
+        task_id = "test_bteq_operator"
+        sql = "SELECT * FROM my_table;"
+        teradata_conn_id = "teradata_default"
+        mock_context = {}
+        # Given
+        expected_result = "BTEQ execution result"
+        mock_execute_bteq.return_value = expected_result
+        operator = BteqOperator(
+            task_id=task_id,
+            sql=sql,
+            teradata_conn_id=teradata_conn_id,
+        )
+
+        # When
+        result = operator.execute(mock_context)
+
+        # Then
+        
mock_hook_init.assert_called_once_with(teradata_conn_id=teradata_conn_id, 
ssh_conn_id=None)
+        mock_execute_bteq.assert_called_once_with(sql + "\n.EXIT", "/tmp", "", 
600, None, "", None, "UTF-8")
+        assert result == "BTEQ execution result"
+
+    @mock.patch.object(BteqHook, "execute_bteq_script")
+    @mock.patch.object(BteqHook, "__init__", return_value=None)
+    def test_execute_sql_only(self, mock_hook_init, mock_execute_bteq):
+        # Arrange
+        task_id = "test_bteq_operator"
+        sql = "SELECT * FROM my_table;"
+        teradata_conn_id = "teradata_default"
+        mock_context = {}
+        expected_result = "BTEQ execution result"
+        mock_execute_bteq.return_value = expected_result
+
+        operator = BteqOperator(
+            task_id=task_id,
+            sql=sql,
+            teradata_conn_id=teradata_conn_id,
+        )
+        # Manually set _hook since we bypassed __init__
+        operator._hook = mock.MagicMock()
+        operator._hook.execute_bteq_script = mock_execute_bteq
+
+        # Act
+        result = operator.execute(mock_context)
+
+        # Assert
+        
mock_hook_init.assert_called_once_with(teradata_conn_id=teradata_conn_id, 
ssh_conn_id=None)
+        mock_execute_bteq.assert_called_once_with(
+            sql + "\n.EXIT",  # Assuming the 
prepare_bteq_script_for_local_execution appends ".EXIT"
+            "/tmp",  # default remote_working_dir
+            "",  # bteq_script_encoding (default ASCII => empty string)
+            600,  # timeout default
+            None,  # timeout_rc
+            "",  # bteq_session_encoding
+            None,  # bteq_quit_rc
+            "UTF-8",
+        )
+        assert result == expected_result
+
+    
@mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.execute_bteq_script")
+    @mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.__init__", 
return_value=None)
+    def test_execute_sql_local(self, mock_hook_init, mock_execute_script):
+        sql = "SELECT * FROM test_table;"
+        expected_result = 0
+        mock_execute_script.return_value = expected_result
+        context = {}
+
+        op = BteqOperator(
+            task_id="test_local_sql",
+            sql=sql,
+            teradata_conn_id="td_conn",
+        )
+        op._hook = mock.Mock()
+        op._hook.execute_bteq_script = mock_execute_script
+
+        result = op.execute(context)
+
+        mock_hook_init.assert_called_once_with(teradata_conn_id="td_conn", 
ssh_conn_id=None)
+        mock_execute_script.assert_called_once()
+        assert result == expected_result
+
+    @mock.patch.object(BteqHook, "on_kill")
+    def test_on_kill(self, mock_on_kill):
+        task_id = "test_bteq_operator"
+        sql = "SELECT * FROM my_table;"
+        # Given
+        operator = BteqOperator(
+            task_id=task_id,
+            sql=sql,
+        )
+        operator._hook = BteqHook(None)
+
+        # When
+        operator.on_kill()
+
+        # Then
+        mock_on_kill.assert_called_once()
+
+    def test_on_kill_not_initialized(self):
+        task_id = "test_bteq_operator"
+        sql = "SELECT * FROM my_table;"
+        # Given
+        operator = BteqOperator(
+            task_id=task_id,
+            sql=sql,
+        )
+        operator._hook = None
+
+        # When/Then (no exception should be raised)
+        operator.on_kill()
+
+    def test_template_fields(self):
+        # Verify template fields are defined correctly
+        print(BteqOperator.template_fields)
+        assert BteqOperator.template_fields == "sql"
+
+    def test_execute_raises_if_no_sql_or_file(self):
+        op = BteqOperator(task_id="fail_case", teradata_conn_id="td_conn")
+        with pytest.raises(ValueError, match="requires either the 'sql' or 
'file_path' parameter"):
+            op.execute({})
+
+    @mock.patch("airflow.providers.teradata.operators.bteq.is_valid_file", 
return_value=False)
+    def test_invalid_file_path(self, mock_is_valid_file):
+        op = BteqOperator(
+            task_id="fail_invalid_file",
+            file_path="/invalid/path.sql",
+            teradata_conn_id="td_conn",
+        )
+        with pytest.raises(ValueError, match="is invalid or does not exist"):
+            op.execute({})
+
+    @mock.patch("airflow.providers.teradata.operators.bteq.is_valid_file", 
return_value=True)
+    @mock.patch(
+        "airflow.providers.teradata.operators.bteq.is_valid_encoding",
+        side_effect=UnicodeDecodeError("utf8", b"", 0, 1, "error"),
+    )
+    def test_file_encoding_error(self, mock_encoding, mock_valid_file):
+        op = BteqOperator(
+            task_id="encoding_fail",
+            file_path="/tmp/test.sql",
+            bteq_script_encoding="UTF-8",
+            teradata_conn_id="td_conn",
+        )
+        with pytest.raises(ValueError, match="encoding is different from BTEQ 
I/O encoding"):
+            op.execute({})
+
+    
@mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.execute_bteq_script")
+    @mock.patch("airflow.providers.teradata.operators.bteq.is_valid_file", 
return_value=True)
+    @mock.patch("airflow.providers.teradata.operators.bteq.is_valid_encoding")
+    @mock.patch("airflow.providers.teradata.operators.bteq.read_file")
+    def test_execute_local_file(
+        self,
+        mock_read_file,
+        mock_valid_encoding,
+        mock_valid_file,
+        mock_execute_bteq_script,
+    ):
+        mock_execute_bteq_script.return_value = 0
+        sql_content = "SELECT * FROM table_name;"
+        mock_read_file.return_value = sql_content
+
+        with tempfile.NamedTemporaryFile("w+", suffix=".sql", delete=False) as 
tmp_file:
+            tmp_file.write(sql_content)
+            tmp_file_path = tmp_file.name
+
+        op = BteqOperator(
+            task_id="test_bteq_local_file",
+            file_path=tmp_file_path,
+            teradata_conn_id="teradata_default",
+        )
+
+        result = op.execute(context={})
+
+        assert result == 0
+        mock_execute_bteq_script.assert_called_once()
+
+    def test_on_kill_calls_hook(self):
+        op = BteqOperator(task_id="kill_test", teradata_conn_id="td_conn")
+        op._hook = mock.Mock()
+        op.on_kill()
+        op._hook.on_kill.assert_called_once()
+
+    def test_on_kill_logs_if_no_hook(self):
+        op = BteqOperator(task_id="kill_no_hook", teradata_conn_id="td_conn")
+        op._hook = None
+
+        with mock.patch.object(op.log, "warning") as mock_log_info:
+            op.on_kill()
+            mock_log_info.assert_called_once_with("BteqHook was not 
initialized. Nothing to terminate.")
+
+    
@mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.execute_bteq_script")
+    @mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.get_conn")
+    @mock.patch("airflow.providers.teradata.operators.bteq.SSHHook")
+    @mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.__init__", 
return_value=None)
+    def test_remote_execution_with_sql(
+        self,
+        mock_bteq_hook_init,
+        mock_ssh_hook_class,
+        mock_get_conn,
+        mock_execute_bteq_script,
+    ):
+        mock_execute_bteq_script.return_value = 0
+        mock_ssh_hook_instance = mock.Mock()
+        mock_ssh_hook_class.return_value = mock_ssh_hook_instance
+
+        op = BteqOperator(
+            task_id="test_remote_sql",
+            sql="SELECT * FROM customers;",
+            ssh_conn_id="ssh_default",
+            teradata_conn_id="teradata_default",
+        )
+
+        result = op.execute(context={})
+
+        mock_bteq_hook_init.assert_called_once_with(
+            teradata_conn_id="teradata_default", ssh_conn_id="ssh_default"
+        )
+        mock_execute_bteq_script.assert_called_once()
+        assert result == 0
+
+    @mock.patch("airflow.models.BaseOperator.render_template")
+    def test_render_template_in_sql(self, mock_render):
+        op = BteqOperator(task_id="render_test", sql="SELECT * FROM {{ 
params.table }};")
+        mock_render.return_value = "SELECT * FROM my_table;"
+        rendered_sql = op.render_template("sql", op.sql, context={"params": 
{"table": "my_table"}})
+        assert rendered_sql == "SELECT * FROM my_table;"
+
+    
@mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.execute_bteq_script",
 return_value=99)
+    @mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.__init__", 
return_value=None)
+    def test_bteq_timeout_with_custom_rc(self, mock_hook_init, mock_exec):
+        op = BteqOperator(
+            task_id="timeout_case",
+            sql="SELECT 1",
+            teradata_conn_id="td_conn",
+            timeout=30,
+            timeout_rc=99,
+            bteq_quit_rc=[99],
+        )
+        result = op.execute({})
+        assert result == 99
+        mock_exec.assert_called_once()
+
+    
@mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.execute_bteq_script",
 return_value=42)
+    @mock.patch("airflow.providers.teradata.operators.bteq.BteqHook.__init__", 
return_value=None)
+    def test_bteq_return_code_not_in_quit_rc(self, mock_hook_init, mock_exec):
+        op = BteqOperator(
+            task_id="rc_not_allowed", sql="SELECT 1", 
teradata_conn_id="td_conn", bteq_quit_rc=[0, 1]
+        )
+        result = op.execute({})
+        assert result == 42  # still returns, but caller can fail on RC if 
desired
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/providers/teradata/tests/unit/teradata/utils/test_bteq_util.py 
b/providers/teradata/tests/unit/teradata/utils/test_bteq_util.py
new file mode 100644
index 00000000000..f0ee54aaa68
--- /dev/null
+++ b/providers/teradata/tests/unit/teradata/utils/test_bteq_util.py
@@ -0,0 +1,187 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import stat
+import unittest
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from airflow.exceptions import AirflowException
+from airflow.providers.teradata.utils.bteq_util import (
+    is_valid_encoding,
+    is_valid_file,
+    is_valid_remote_bteq_script_file,
+    prepare_bteq_script_for_local_execution,
+    prepare_bteq_script_for_remote_execution,
+    read_file,
+    transfer_file_sftp,
+    verify_bteq_installed,
+    verify_bteq_installed_remote,
+)
+
+
+class TestBteqUtils:
+    @patch("shutil.which")
+    def test_verify_bteq_installed_success(self, mock_which):
+        mock_which.return_value = "/usr/bin/bteq"
+        # Should not raise
+        verify_bteq_installed()
+        mock_which.assert_called_with("bteq")
+
+    @patch("shutil.which")
+    def test_verify_bteq_installed_fail(self, mock_which):
+        mock_which.return_value = None
+        with pytest.raises(AirflowException):
+            verify_bteq_installed()
+
+    def test_prepare_bteq_script_for_remote_execution(self):
+        conn = {"host": "myhost", "login": "user", "password": "pass"}
+        sql = "SELECT * FROM DUAL;"
+        script = prepare_bteq_script_for_remote_execution(conn, sql)
+        assert ".LOGON myhost/user,pass" in script
+        assert "SELECT * FROM DUAL;" in script
+        assert ".EXIT" in script
+
+    def test_prepare_bteq_script_for_local_execution(self):
+        sql = "SELECT 1;"
+        script = prepare_bteq_script_for_local_execution(sql)
+        assert "SELECT 1;" in script
+        assert ".EXIT" in script
+
+    @patch("paramiko.SSHClient.exec_command")
+    def test_verify_bteq_installed_remote_success(self, mock_exec):
+        mock_stdin = MagicMock()
+        mock_stdout = MagicMock()
+        mock_stderr = MagicMock()
+        mock_stdout.channel.recv_exit_status.return_value = 0
+        mock_stdout.read.return_value = b"/usr/bin/bteq"
+        mock_stderr.read.return_value = b""
+        mock_exec.return_value = (mock_stdin, mock_stdout, mock_stderr)
+
+        ssh_client = MagicMock()
+        ssh_client.exec_command = mock_exec
+
+        # Should not raise
+        verify_bteq_installed_remote(ssh_client)
+
+    @patch("paramiko.SSHClient.exec_command")
+    def test_verify_bteq_installed_remote_fail(self, mock_exec):
+        mock_stdin = MagicMock()
+        mock_stdout = MagicMock()
+        mock_stderr = MagicMock()
+        mock_stdout.channel.recv_exit_status.return_value = 1
+        mock_stdout.read.return_value = b""
+        mock_stderr.read.return_value = b"command not found"
+        mock_exec.return_value = (mock_stdin, mock_stdout, mock_stderr)
+
+        ssh_client = MagicMock()
+        ssh_client.exec_command = mock_exec
+
+        with pytest.raises(AirflowException):
+            verify_bteq_installed_remote(ssh_client)
+
+    @patch("paramiko.SSHClient.open_sftp")
+    def test_transfer_file_sftp(self, mock_open_sftp):
+        mock_sftp = MagicMock()
+        mock_open_sftp.return_value = mock_sftp
+
+        ssh_client = MagicMock()
+        ssh_client.open_sftp = mock_open_sftp
+
+        transfer_file_sftp(ssh_client, "local_file.txt", "remote_file.txt")
+
+        mock_open_sftp.assert_called_once()
+        mock_sftp.put.assert_called_once_with("local_file.txt", 
"remote_file.txt")
+        mock_sftp.close.assert_called_once()
+
+    def test_is_valid_file(self):
+        # create temp file
+        with open("temp_test_file.txt", "w") as f:
+            f.write("hello")
+
+        assert is_valid_file("temp_test_file.txt") is True
+        assert is_valid_file("non_existent_file.txt") is False
+
+        os.remove("temp_test_file.txt")
+
+    def test_is_valid_encoding(self):
+        # Write a file with UTF-8 encoding
+        with open("temp_utf8_file.txt", "w", encoding="utf-8") as f:
+            f.write("hello world")
+
+        # Should return True
+        assert is_valid_encoding("temp_utf8_file.txt", encoding="utf-8") is 
True
+
+        # Cleanup
+        os.remove("temp_utf8_file.txt")
+
+    def test_read_file_success(self):
+        content = "Sample content"
+        with open("temp_read_file.txt", "w") as f:
+            f.write(content)
+
+        read_content = read_file("temp_read_file.txt")
+        assert read_content == content
+        os.remove("temp_read_file.txt")
+
+    def test_read_file_file_not_found(self):
+        with pytest.raises(FileNotFoundError):
+            read_file("non_existent_file.txt")
+
+    @patch("paramiko.SSHClient.open_sftp")
+    def test_is_valid_remote_bteq_script_file_exists(self, mock_open_sftp):
+        mock_sftp = MagicMock()
+        mock_open_sftp.return_value = mock_sftp
+
+        # Mock stat to return a regular file mode
+        mock_stat = MagicMock()
+        mock_stat.st_mode = stat.S_IFREG
+        mock_sftp.stat.return_value = mock_stat
+
+        ssh_client = MagicMock()
+        ssh_client.open_sftp = mock_open_sftp
+
+        result = is_valid_remote_bteq_script_file(ssh_client, 
"/remote/path/to/file")
+        assert result is True
+        mock_sftp.close.assert_called_once()
+
+    @patch("paramiko.SSHClient.open_sftp")
+    def test_is_valid_remote_bteq_script_file_not_exists(self, mock_open_sftp):
+        mock_sftp = MagicMock()
+        mock_open_sftp.return_value = mock_sftp
+
+        # Raise FileNotFoundError for stat
+        mock_sftp.stat.side_effect = FileNotFoundError
+
+        ssh_client = MagicMock()
+        ssh_client.open_sftp = mock_open_sftp
+
+        result = is_valid_remote_bteq_script_file(ssh_client, 
"/remote/path/to/file")
+        assert result is False
+        mock_sftp.close.assert_called_once()
+
+    def test_is_valid_remote_bteq_script_file_none_path(self):
+        ssh_client = MagicMock()
+        result = is_valid_remote_bteq_script_file(ssh_client, None)
+        assert result is False
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git 
a/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py 
b/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py
new file mode 100644
index 00000000000..ba688182ae6
--- /dev/null
+++ b/providers/teradata/tests/unit/teradata/utils/test_encryption_utils.py
@@ -0,0 +1,108 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import string
+import unittest
+from unittest.mock import MagicMock, patch
+
+from airflow.providers.teradata.utils.encryption_utils import (
+    decrypt_remote_file_to_string,
+    generate_encrypted_file_with_openssl,
+    generate_random_password,
+    shell_quote_single,
+)
+
+
+class TestEncryptionUtils:
+    def test_generate_random_password_length(self):
+        pwd = generate_random_password(16)
+        assert len(pwd) == 16
+        # Check characters are in allowed set
+        allowed_chars = string.ascii_letters + string.digits + 
string.punctuation
+        assert (all(c in allowed_chars for c in pwd)) is True
+
+    @patch("subprocess.run")
+    def test_generate_encrypted_file_with_openssl_calls_subprocess(self, 
mock_run):
+        file_path = "/tmp/plain.txt"
+        password = "testpass"
+        out_file = "/tmp/encrypted.enc"
+
+        generate_encrypted_file_with_openssl(file_path, password, out_file)
+
+        mock_run.assert_called_once_with(
+            [
+                "openssl",
+                "enc",
+                "-aes-256-cbc",
+                "-salt",
+                "-pbkdf2",
+                "-pass",
+                f"pass:{password}",
+                "-in",
+                file_path,
+                "-out",
+                out_file,
+            ],
+            check=True,
+        )
+
+    def test_shell_quote_single_simple(self):
+        s = "simple"
+        quoted = shell_quote_single(s)
+        assert quoted == "'simple'"
+
+    def test_shell_quote_single_with_single_quote(self):
+        s = "O'Reilly"
+        quoted = shell_quote_single(s)
+        assert quoted == "'O'\\''Reilly'"
+
+    def test_decrypt_remote_file_to_string(self):
+        password = "mysecret"
+        remote_enc_file = "/remote/encrypted.enc"
+        bteq_command_str = "bteq -c UTF-8"
+
+        ssh_client = MagicMock()
+        mock_stdin = MagicMock()
+        mock_stdout = MagicMock()
+        mock_stderr = MagicMock()
+
+        # Setup mock outputs and exit code
+        mock_stdout.channel.recv_exit_status.return_value = 0
+        mock_stdout.read.return_value = b"decrypted output"
+        mock_stderr.read.return_value = b""
+
+        ssh_client.exec_command.return_value = (mock_stdin, mock_stdout, 
mock_stderr)
+
+        exit_status, output, err = decrypt_remote_file_to_string(
+            ssh_client, remote_enc_file, password, bteq_command_str
+        )
+
+        quoted_password = shell_quote_single(password)
+        expected_cmd = (
+            f"openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass 
pass:{quoted_password} -in {remote_enc_file} | "
+            + bteq_command_str
+        )
+
+        ssh_client.exec_command.assert_called_once_with(expected_cmd)
+        assert exit_status == 0
+        assert output == "decrypted output"
+        assert err == ""
+
+
+if __name__ == "__main__":
+    unittest.main()

Reply via email to