From 035774e9208c5aeebf8f2491e6cdd2f613a382f0 Mon Sep 17 00:00:00 2001
From: Nicola Coretti <nicola.coretti@exasol.com>
Date: Wed, 26 Jun 2024 13:03:47 +0200
Subject: [PATCH 1/7] Add basic test setup for ETL related functionality

---
 pyproject.toml                  |  1 +
 test/integration/etl_test.py    | 90 +++++++++++++++++++++++++++++++++
 test/integration/export_test.py | 26 ++++++++++
 test/integration/import_test.py | 21 ++++++++
 4 files changed, 138 insertions(+)
 create mode 100644 test/integration/etl_test.py
 create mode 100644 test/integration/export_test.py
 create mode 100644 test/integration/import_test.py

diff --git a/pyproject.toml b/pyproject.toml
index 506b36a..ab2513b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -89,6 +89,7 @@ markers = [
     "tls: tests related to tls.",
     "udf: tests related to user defined functions.",
     "pandas: tests related to support of pandas library.",
+    "etl: etl related tests, export, load (import) and transform data.",
     "misc: miscellaneous tests which did not fit in the other categories."
 ]
 
diff --git a/test/integration/etl_test.py b/test/integration/etl_test.py
new file mode 100644
index 0000000..17bf4e9
--- /dev/null
+++ b/test/integration/etl_test.py
@@ -0,0 +1,90 @@
+import pytest
+import pyexasol
+from inspect import cleandoc
+
+
+@pytest.fixture
+def connection(dsn, user, password, schema):
+    with pyexasol.connect(
+        dsn=dsn, user=user, password=password, schema=schema, compression=True
+    ) as con:
+        yield con
+
+
+@pytest.fixture
+def export_file(tmp_path):
+    yield tmp_path / "export"
+
+
+@pytest.fixture
+def table(connection):
+    yield "USERS"
+
+
+@pytest.fixture
+def import_table(connection, table):
+    name = f"{table}_IMPORT"
+    ddl = cleandoc(f"""
+    CREATE TABLE IF NOT EXISTS {name}
+    (
+        user_id         DECIMAL(18,0),
+        user_name       VARCHAR(255),
+        register_dt     DATE,
+        last_visit_ts   TIMESTAMP,
+        is_female       BOOLEAN,
+        user_rating     DECIMAL(10,5),
+        user_score      DOUBLE,
+        status          VARCHAR(50)
+    );
+    """)
+    connection.execute(ddl)
+    connection.commit()
+
+    yield name
+
+    ddl = f"DROP TABLE IF EXISTS {name};"
+    connection.execute(ddl)
+    connection.commit()
+
+
+@pytest.mark.etl
+@pytest.mark.parametrize(
+    "params,expected",
+    [
+        ({}, []),
+        ({"format": "gz"}, []),
+        ({"encoding": "WINDOWS-1251"}, []),
+        ({"columns": ["register_dt", "user_id", "status", "user_name"]}, []),
+        (
+            {
+                "csv_cols": [
+                    "1",
+                    "2",
+                    "3 FORMAT='DD-MM-YYYY'",
+                    "4..6",
+                    "7 FORMAT='999.99999'",
+                    "8",
+                ]
+            },
+            [],
+        ),
+    ],
+)
+def test_export_import_round_trip_to_and_from_file(
+    connection, export_file, table, import_table, params, expected
+):
+    connection.export_to_file(export_file, table, export_params=params)
+    connection.import_from_file(export_file, import_table, import_params=params)
+
+    query = cleandoc(
+        f"""
+        SELECT * FROM {table}
+        WHERE USER_ID NOT IN (SELECT USER_ID from {import_table});
+        """
+    )
+    result = connection.execute(query)
+
+    expected = []
+    actual = result.fetchall()
+
+    assert actual == expected
diff --git a/test/integration/export_test.py b/test/integration/export_test.py
new file mode 100644
index 0000000..46ae787
--- /dev/null
+++ b/test/integration/export_test.py
@@ -0,0 +1,26 @@
+import pytest
+
+
+@pytest.mark.etl
+def test_export_with_column_names():
+    assert False
+
+
+@pytest.mark.etl
+def test_skip_rows_in_export():
+    assert False
+
+
+@pytest.mark.etl
+def test_custom_export_callback():
+    assert False
+
+
+@pytest.mark.etl
+def test_export_with_reodered_columns():
+    assert False
+
+
+@pytest.mark.etl
+def test_export_with_custom_csv_format():
+    assert False
diff --git a/test/integration/import_test.py b/test/integration/import_test.py
new file mode 100644
index 0000000..658ea30
--- /dev/null
+++ b/test/integration/import_test.py
@@ -0,0 +1,21 @@
+import pytest
+
+
+@pytest.mark.etl
+def test_import_with_custom_csv_format():
+    assert False
+
+
+@pytest.mark.etl
+def test_import_with_reordered_columns():
+    assert False
+
+
+@pytest.mark.etl
+def test_custom_import_callback():
+    assert False
+
+
+@pytest.mark.etl
+def test_skip_rows_in_import():
+    assert False

From 27306dfbd159f38a8331feb1d17ce063f42c9542 Mon Sep 17 00:00:00 2001
From: Nicola Coretti <nicola.coretti@exasol.com>
Date: Fri, 28 Jun 2024 11:24:27 +0200
Subject: [PATCH 2/7] Add import tests

---
 test/integration/import_test.py | 101 +++++++++++++++++++++++++++++---
 1 file changed, 93 insertions(+), 8 deletions(-)

diff --git a/test/integration/import_test.py b/test/integration/import_test.py
index 658ea30..23c4637 100644
--- a/test/integration/import_test.py
+++ b/test/integration/import_test.py
@@ -1,21 +1,106 @@
+import csv
 import pytest
+import pyexasol
+from inspect import cleandoc
+
+
+@pytest.fixture
+def connection(dsn, user, password, schema):
+    with pyexasol.connect(
+        dsn=dsn, user=user, password=password, schema=schema, compression=True
+    ) as con:
+        yield con
+
+
+@pytest.fixture
+def table_name():
+    yield "CLIENT_NAMES"
+
+
+@pytest.fixture
+def empty_table(connection, table_name):
+    ddl = cleandoc(f"""
+    CREATE TABLE IF NOT EXISTS {table_name}
+    (
+        FIRSTNAME       VARCHAR(255),
+        LASTNAME        VARCHAR(255)
+    );
+    """)
+    connection.execute(ddl)
+    connection.commit()
+
+    yield table_name
+
+    ddl = f"DROP TABLE IF EXISTS {table_name};"
+    connection.execute(ddl)
+    connection.commit()
+
+
+@pytest.fixture
+def data(faker):
+    yield [(faker.first_name(), faker.last_name()) for _ in range(0, 10)]
+
+
+@pytest.fixture
+def swaped_data(data):
+    yield [(lastname, firstname) for firstname, lastname in data]
+
+
+@pytest.fixture
+def csv_file(tmp_path, data):
+    file = tmp_path / "names.csv"
+    with open(file, "w", newline="") as csvfile:
+        writer = csv.writer(csvfile, dialect="unix")
+        for row in data:
+            writer.writerow(row)
+    yield file
 
 
 @pytest.mark.etl
-def test_import_with_custom_csv_format():
-    assert False
+def test_import_with_custom_csv_format(connection, empty_table, csv_file, data):
+    connection.import_from_file(csv_file, empty_table)
+    result = connection.execute(f"SELECT * FROM {empty_table};")
+
+    expected = set(data)
+    actual = set(result.fetchall())
+
+    assert actual == expected
 
 
 @pytest.mark.etl
-def test_import_with_reordered_columns():
-    assert False
+def test_import_with_reordered_columns(connection, empty_table, csv_file, swaped_data):
+    params = {"columns": ["LASTNAME", "FIRSTNAME"]}
+    connection.import_from_file(csv_file, empty_table, import_params=params)
+    result = connection.execute(f"SELECT * FROM {empty_table};")
+
+    expected = set(swaped_data)
+    actual = set(result.fetchall())
+
+    assert actual == expected
 
 
 @pytest.mark.etl
-def test_custom_import_callback():
-    assert False
+def test_custom_import_callback(connection, empty_table, csv_file, data):
+    def import_cb(pipe, src):
+        pipe.write(src.read_bytes())
+
+    connection.import_from_callback(import_cb, csv_file, empty_table)
+    result = connection.execute(f"SELECT * FROM {empty_table};")
+
+    expected = set(data)
+    actual = set(result.fetchall())
+
+    assert actual == expected
 
 
 @pytest.mark.etl
-def test_skip_rows_in_import():
-    assert False
+def test_skip_rows_in_import(connection, empty_table, csv_file, data):
+    offset = 2
+    params = {"skip": offset}
+    connection.import_from_file(csv_file, empty_table, import_params=params)
+    result = connection.execute(f"SELECT * FROM {empty_table};")
+
+    expected = set(data[offset::])
+    actual = set(result.fetchall())
+
+    assert actual == expected

From 24a55150d28ed0926b245c287065c2f80f7dd0cb Mon Sep 17 00:00:00 2001
From: Nicola Coretti <nicola.coretti@exasol.com>
Date: Mon, 1 Jul 2024 08:36:23 +0200
Subject: [PATCH 3/7] WIP

---
 test/integration/export_test.py | 72 ++++++++++++++++++++++++++++++++-
 1 file changed, 70 insertions(+), 2 deletions(-)

diff --git a/test/integration/export_test.py b/test/integration/export_test.py
index 46ae787..cc9f9a0 100644
--- a/test/integration/export_test.py
+++ b/test/integration/export_test.py
@@ -1,4 +1,64 @@
+import csv
 import pytest
+import pyexasol
+from inspect import cleandoc
+
+
+@pytest.fixture
+def connection(dsn, user, password, schema):
+    with pyexasol.connect(
+        dsn=dsn, user=user, password=password, schema=schema, compression=True
+    ) as con:
+        yield con
+
+
+@pytest.fixture
+def table_name():
+    yield "CLIENT_NAMES"
+
+
+@pytest.fixture
+def empty_table(connection, table_name):
+    ddl = cleandoc(f"""
+    CREATE TABLE IF NOT EXISTS {table_name}
+    (
+        FIRSTNAME       VARCHAR(255),
+        LASTNAME        VARCHAR(255)
+    );
+    """)
+    connection.execute(ddl)
+    connection.commit()
+
+    yield table_name
+
+    ddl = f"DROP TABLE IF EXISTS {table_name};"
+    connection.execute(ddl)
+    connection.commit()
+
+
+@pytest.fixture
+def data(faker):
+    yield [(faker.first_name(), faker.last_name()) for _ in range(0, 10)]
+
+
+@pytest.fixture
+def swaped_data(data):
+    yield [(lastname, firstname) for firstname, lastname in data]
+
+
+@pytest.fixture
+def table(connection, empty_table, data):
+    name = empty_table
+    stmt = "INSERT INTO {table} VALUES ({{col1}}, {{col2}});"
+    for col1, col2 in data:
+        connection.execute(stmt.format(table=name), {"col1": col1, "col2":col2})
+    connection.commit()
+    yield name
+
+
+@pytest.fixture
+def export_file(tmp_path, data):
+    yield tmp_path / "names.csv"
 
 
 @pytest.mark.etl
@@ -12,8 +72,16 @@ def test_skip_rows_in_export():
 
 
 @pytest.mark.etl
-def test_custom_export_callback():
-    assert False
+def test_custom_export_callback(connection, table, data, export_file):
+    def export_cb(pipe, dst):
+        dst.write_bytes(pipe.read())
+
+    connection.export_to_callback(export_cb, export_file, table)
+
+    expected = 0 # TBD
+    actual = None # TBD
+
+    assert actual == expected
 
 
 @pytest.mark.etl

From e62dfae510a7e857e17e16189d98cb896fb4f039 Mon Sep 17 00:00:00 2001
From: Nicola Coretti <nicola.coretti@exasol.com>
Date: Mon, 8 Jul 2024 07:50:57 +0200
Subject: [PATCH 4/7] Relock dependencies

---
 poetry.lock | 121 +++++++++++++++++++++++++++-------------------------
 1 file changed, 63 insertions(+), 58 deletions(-)

diff --git a/poetry.lock b/poetry.lock
index a2d36b3..af9078b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -97,13 +97,13 @@ typecheck = ["mypy"]
 
 [[package]]
 name = "certifi"
-version = "2024.6.2"
+version = "2024.7.4"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
-    {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
+    {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
+    {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
 ]
 
 [[package]]
@@ -1009,57 +1009,62 @@ files = [
 
 [[package]]
 name = "orjson"
-version = "3.10.5"
+version = "3.10.6"
 description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
 optional = true
 python-versions = ">=3.8"
 files = [
-    {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"},
-    {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"},
-    {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"},
-    {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"},
-    {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"},
-    {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"},
-    {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"},
-    {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"},
-    {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"},
-    {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"},
-    {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"},
-    {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"},
-    {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"},
-    {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"},
-    {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"},
-    {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"},
-    {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"},
-    {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"},
-    {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"},
-    {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"},
-    {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"},
-    {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"},
-    {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"},
-    {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"},
-    {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"},
-    {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"},
-    {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"},
-    {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"},
-    {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"},
-    {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"},
-    {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"},
-    {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"},
-    {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"},
-    {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"},
-    {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"},
-    {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"},
-    {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"},
-    {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"},
-    {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"},
-    {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"},
-    {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"},
-    {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"},
-    {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"},
-    {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"},
-    {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"},
-    {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"},
+    {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
+    {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
+    {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"},
+    {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"},
+    {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"},
+    {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"},
+    {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"},
+    {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"},
+    {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"},
+    {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"},
+    {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"},
+    {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"},
+    {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"},
+    {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"},
+    {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"},
+    {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"},
+    {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"},
+    {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"},
+    {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"},
+    {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"},
+    {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"},
+    {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"},
+    {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"},
+    {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"},
+    {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"},
+    {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"},
+    {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"},
+    {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
+    {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
+    {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
+    {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
+    {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
+    {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
+    {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"},
+    {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"},
+    {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"},
+    {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"},
+    {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"},
+    {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"},
+    {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"},
+    {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"},
+    {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"},
+    {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"},
+    {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"},
+    {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"},
+    {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"},
+    {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"},
+    {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"},
+    {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"},
+    {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"},
+    {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"},
 ]
 
 [[package]]
@@ -1552,18 +1557,18 @@ pyasn1 = ">=0.1.3"
 
 [[package]]
 name = "setuptools"
-version = "70.1.1"
+version = "70.2.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"},
-    {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"},
+    {file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"},
+    {file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "simplejson"
@@ -1718,13 +1723,13 @@ files = [
 
 [[package]]
 name = "tenacity"
-version = "8.4.2"
+version = "8.5.0"
 description = "Retry code until it succeeds"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "tenacity-8.4.2-py3-none-any.whl", hash = "sha256:9e6f7cf7da729125c7437222f8a522279751cdfbe6b67bfe64f75d3a348661b2"},
-    {file = "tenacity-8.4.2.tar.gz", hash = "sha256:cd80a53a79336edba8489e767f729e4f391c896956b57140b5d7511a64bbd3ef"},
+    {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
+    {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
 ]
 
 [package.extras]

From 0e7cd5f374dff12bd366cc371c2c2c9d58c5c6d4 Mon Sep 17 00:00:00 2001
From: Nicola Coretti <nicola.coretti@exasol.com>
Date: Mon, 8 Jul 2024 12:07:00 +0200
Subject: [PATCH 5/7] Update export tests

---
 test/integration/export_test.py | 55 ++++++++++++++++++++++++---------
 1 file changed, 41 insertions(+), 14 deletions(-)

diff --git a/test/integration/export_test.py b/test/integration/export_test.py
index cc9f9a0..11bc812 100644
--- a/test/integration/export_test.py
+++ b/test/integration/export_test.py
@@ -1,3 +1,4 @@
+import io
 import csv
 import pytest
 import pyexasol
@@ -51,7 +52,7 @@ def table(connection, empty_table, data):
     name = empty_table
     stmt = "INSERT INTO {table} VALUES ({{col1}}, {{col2}});"
     for col1, col2 in data:
-        connection.execute(stmt.format(table=name), {"col1": col1, "col2":col2})
+        connection.execute(stmt.format(table=name), {"col1": col1, "col2": col2})
     connection.commit()
     yield name
 
@@ -61,34 +62,60 @@ def export_file(tmp_path, data):
     yield tmp_path / "names.csv"
 
 
-@pytest.mark.etl
-def test_export_with_column_names():
-    assert False
+@pytest.fixture
+def csv_dialect():
+    class PyexasolCsvDialect(csv.Dialect):
+        lineterminator = "\n"
+        delimiter = ","
+        quoting = csv.QUOTE_MINIMAL
+        quotechar = '"'
+
+    yield PyexasolCsvDialect()
+
+
+@pytest.fixture
+def expected_csv(csv_dialect):
+    def create_csv(table, data, **params):
+        csvfile = io.StringIO()
+        if "with_column_names" in params and params["with_column_names"]:
+            data = [("FIRSTNAME", "LASTNAME")] + data
+        writer = csv.writer(csvfile, csv_dialect)
+        writer.writerows(data)
+        return csvfile.getvalue()
+
+    yield create_csv
 
 
 @pytest.mark.etl
-def test_skip_rows_in_export():
-    assert False
+def test_export_with_column_names(connection, table, data, export_file, expected_csv):
+    params = {"with_column_names": True}
+    connection.export_to_file(export_file, table, export_params=params)
+
+    expected = expected_csv(table, data, **params)
+    actual = export_file.read_text()
+
+    assert actual == expected
 
 
 @pytest.mark.etl
-def test_custom_export_callback(connection, table, data, export_file):
+def test_custom_export_callback(connection, table, data, export_file, expected_csv):
     def export_cb(pipe, dst):
         dst.write_bytes(pipe.read())
 
     connection.export_to_callback(export_cb, export_file, table)
 
-    expected = 0 # TBD
-    actual = None # TBD
+    expected = expected_csv(table, data)
+    actual = export_file.read_text()
 
     assert actual == expected
 
 
 @pytest.mark.etl
-def test_export_with_reodered_columns():
-    assert False
+def test_export_csv_cols(connection, table, data, export_file, expected_csv):
+    params = {"csv_cols": ["1..2", "2 foo_LASTNAME"]}
+    connection.export_to_file(export_file, table, export_params=params)
 
+    expected = expected_csv(table, data, **params)
+    actual = export_file.read_text()
 
-@pytest.mark.etl
-def test_export_with_custom_csv_format():
-    assert False
+    assert actual == expected

From 6a8b50face368f91a29525a2b40b5c3dbbba4057 Mon Sep 17 00:00:00 2001
From: Nicola Coretti <nicola.coretti@exasol.com>
Date: Tue, 9 Jul 2024 07:57:40 +0200
Subject: [PATCH 6/7] Fix csv_cols test

---
 test/integration/export_test.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/integration/export_test.py b/test/integration/export_test.py
index 11bc812..6d16302 100644
--- a/test/integration/export_test.py
+++ b/test/integration/export_test.py
@@ -112,7 +112,7 @@ def export_cb(pipe, dst):
 
 @pytest.mark.etl
 def test_export_csv_cols(connection, table, data, export_file, expected_csv):
-    params = {"csv_cols": ["1..2", "2 foo_LASTNAME"]}
+    params = {"csv_cols": ["1..2"]}
     connection.export_to_file(export_file, table, export_params=params)
 
     expected = expected_csv(table, data, **params)

From 26e9211fe4a0073adbbd7719c48d5494f84837fd Mon Sep 17 00:00:00 2001
From: Nicola Coretti <nicola.coretti@exasol.com>
Date: Tue, 9 Jul 2024 08:10:56 +0200
Subject: [PATCH 7/7] Apply review feedback

---
 test/integration/etl_test.py    | 35 +++++++++++++++------------------
 test/integration/export_test.py |  5 -----
 test/integration/import_test.py |  2 +-
 3 files changed, 17 insertions(+), 25 deletions(-)

diff --git a/test/integration/etl_test.py b/test/integration/etl_test.py
index 17bf4e9..1d192f9 100644
--- a/test/integration/etl_test.py
+++ b/test/integration/etl_test.py
@@ -49,29 +49,26 @@ def import_table(connection, table):
 
 @pytest.mark.etl
 @pytest.mark.parametrize(
-    "params,expected",
+    "params",
     [
-        ({}, []),
-        ({"format": "gz"}, []),
-        ({"encoding": "WINDOWS-1251"}, []),
-        ({"columns": ["register_dt", "user_id", "status", "user_name"]}, []),
-        (
-            {
-                "csv_cols": [
-                    "1",
-                    "2",
-                    "3 FORMAT='DD-MM-YYYY'",
-                    "4..6",
-                    "7 FORMAT='999.99999'",
-                    "8",
-                ]
-            },
-            [],
-        ),
+        {},
+        {"format": "gz"},
+        {"encoding": "WINDOWS-1251"},
+        {"columns": ["register_dt", "user_id", "status", "user_name"]},
+        {
+            "csv_cols": [
+                "1",
+                "2",
+                "3 FORMAT='DD-MM-YYYY'",
+                "4..6",
+                "7 FORMAT='999.99999'",
+                "8",
+            ]
+        },
     ],
 )
 def test_export_import_round_trip_to_and_from_file(
-    connection, export_file, table, import_table, params, expected
+    connection, export_file, table, import_table, params
 ):
     connection.export_to_file(export_file, table, export_params=params)
     connection.import_from_file(export_file, import_table, import_params=params)
diff --git a/test/integration/export_test.py b/test/integration/export_test.py
index 6d16302..91d1f30 100644
--- a/test/integration/export_test.py
+++ b/test/integration/export_test.py
@@ -42,11 +42,6 @@ def data(faker):
     yield [(faker.first_name(), faker.last_name()) for _ in range(0, 10)]
 
 
-@pytest.fixture
-def swaped_data(data):
-    yield [(lastname, firstname) for firstname, lastname in data]
-
-
 @pytest.fixture
 def table(connection, empty_table, data):
     name = empty_table
diff --git a/test/integration/import_test.py b/test/integration/import_test.py
index 23c4637..ac90466 100644
--- a/test/integration/import_test.py
+++ b/test/integration/import_test.py
@@ -57,7 +57,7 @@ def csv_file(tmp_path, data):
 
 
 @pytest.mark.etl
-def test_import_with_custom_csv_format(connection, empty_table, csv_file, data):
+def test_import_csv(connection, empty_table, csv_file, data):
     connection.import_from_file(csv_file, empty_table)
     result = connection.execute(f"SELECT * FROM {empty_table};")