Repository: statisticsnorway/ssb-eimerdb (language: python)

Issue:
Bump crazy-max/ghaction-github-labeler from 4.2.0 to 5.0.0
Bumps [crazy-max/ghaction-github-labeler](https://github.com/crazy-max/ghaction-github-labeler) from 4.2.0 to 5.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/crazy-max/ghaction-github-labeler/releases">crazy-max/ghaction-github-labeler's releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<ul>
<li>Node 20 as default runtime (requires <a href="https://github.com/actions/runner/releases/tag/v2.308.0">Actions Runner v2.308.0</a> or later) by <a href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a href="https://redirect.github.com/crazy-max/ghaction-github-labeler/pull/199">crazy-max/ghaction-github-labeler#199</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/crazy-max/ghaction-github-labeler/compare/v4.2.0...v5.0.0">https://github.com/crazy-max/ghaction-github-labeler/compare/v4.2.0...v5.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/de749cf181958193cb7debf1a9c5bb28922f3e1b"><code>de749cf</code></a> Merge pull request <a href="https://redirect.github.com/crazy-max/ghaction-github-labeler/issues/198">#198</a> from crazy-max/dependabot/github_actions/actions/chec...</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/7381649272bb633d6d51bc893dbb478b64a54278"><code>7381649</code></a> Merge pull request <a href="https://redirect.github.com/crazy-max/ghaction-github-labeler/issues/199">#199</a> from crazy-max/update-node20</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/e4b824f001d7a5a367db3116dcae44cc29ef8934"><code>e4b824f</code></a> chore: node 20 as default runtime</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/f20248f4707ddc0f4cd54d026fd11fbf8cf357c5"><code>f20248f</code></a> chore: update generated content</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/2fae9f603259c9014c0275201fb7e98f5670fb4e"><code>2fae9f6</code></a> chore: update dev dependencies</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/7ce1845c0dd581c2e9b7521845fcf4579fe23f96"><code>7ce1845</code></a> chore: update to node 20</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/9230c22469d8f9d811b36a6234f72bb5b0e52a06"><code>9230c22</code></a> chore: update yarn to 3.6.3</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/f6c8f774418afb5ff5d2c47b92fe554f774c9512"><code>f6c8f77</code></a> codecov: update config</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/3bd04ae614ce826f2ab0f68fdbaf504f481f3ce4"><code>3bd04ae</code></a> Bump actions/checkout from 3 to 4</li>
<li>See full diff in <a href="https://github.com/crazy-max/ghaction-github-labeler/compare/v4.2.0...v5.0.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crazy-max/ghaction-github-labeler&package-manager=github_actions&previous-version=4.2.0&new-version=5.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>
Bump pytest from 7.4.1 to 7.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.1 to 7.4.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/pytest-dev/pytest/releases">pytest's releases</a>.</em></p>
<blockquote>
<h2>pytest 7.4.2 (2023-09-07)</h2>
<h1>Bug Fixes</h1>
<ul>
<li>
<p><a href="https://redirect.github.com/pytest-dev/pytest/issues/11237">#11237</a>: Fix doctest collection of <code>functools.cached_property</code> objects.</p>
</li>
<li>
<p><a href="https://redirect.github.com/pytest-dev/pytest/issues/11306">#11306</a>: Fixed bug using <code>--importmode=importlib</code> which would cause package <code>__init__.py</code> files to be imported more than once in some cases.</p>
</li>
<li>
<p><a href="https://redirect.github.com/pytest-dev/pytest/issues/11367">#11367</a>: Fixed bug where <code>user_properties</code> where not being saved in the JUnit XML file if a fixture failed during teardown.</p>
</li>
<li>
<p><a href="https://redirect.github.com/pytest-dev/pytest/issues/11394">#11394</a>: Fixed crash when parsing long command line arguments that might be interpreted as files.</p>
</li>
</ul>
<h1>Improved Documentation</h1>
<ul>
<li><a href="https://redirect.github.com/pytest-dev/pytest/issues/11391">#11391</a>: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/pytest-dev/pytest/commit/45f34dfb8d813b00c7d8060a3ea75f7b4bc5cd7d"><code>45f34df</code></a> Prepare release version 7.4.2</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/e4f022f0d82bd530c3e4409c1c6a87e55ef9098d"><code>e4f022f</code></a> Merge pull request <a href="https://redirect.github.com/pytest-dev/pytest/issues/11406">#11406</a> from nicoddemus/backport-11404-to-7.4.x</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/63b0c6f75f218b400a5a42305f9fa3830448f7e8"><code>63b0c6f</code></a> Use _pytest.pathlib.safe_exists in get_dirs_from_args</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/884b911a9cbf3c97689b473ae7f732fc72e06eea"><code>884b911</code></a> Fix crash when passing a very long cmdline argument (<a href="https://redirect.github.com/pytest-dev/pytest/issues/11404">#11404</a>)</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/6e49a74089540196ae2b2bf8267a530ad9957f3e"><code>6e49a74</code></a> [7.4.x] Fix doctest collection of <code>functools.cached_property</code> objects. (<a href="https://redirect.github.com/pytest-dev/pytest/issues/11403">#11403</a>)</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/79c2012d4090bbe0cd0910e1616dc19ca75aea86"><code>79c2012</code></a> [7.4.x] doc: Remove done training (<a href="https://redirect.github.com/pytest-dev/pytest/issues/11400">#11400</a>)</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/de69883e3a6a923c9eceb28cedf8e9660bd58a51"><code>de69883</code></a> [7.4.x] improve plugin list disclaimer (<a href="https://redirect.github.com/pytest-dev/pytest/issues/11398">#11398</a>)</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/1de00e9830c57805ec07a0d67095822703b90bb4"><code>1de00e9</code></a> [7.4.x] Fix import_path for packages (<a href="https://redirect.github.com/pytest-dev/pytest/issues/11395">#11395</a>)</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/7f5d9b9df4e5e323fe3a66aeb956557710e5d277"><code>7f5d9b9</code></a> Fix user_properties not saved to XML if fixture errors during teardown (<a href="https://redirect.github.com/pytest-dev/pytest/issues/11382">#11382</a>)</li>
<li><a href="https://github.com/pytest-dev/pytest/commit/82eb86f707cac79f33d0ff69723ccf1cea7e75f7"><code>82eb86f</code></a> Merge pull request <a href="https://redirect.github.com/pytest-dev/pytest/issues/11377">#11377</a> from pytest-dev/release-7.4.1</li>
<li>Additional commits viewable in <a href="https://github.com/pytest-dev/pytest/compare/7.4.1...7.4.2">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest&package-manager=pip&previous-version=7.4.1&new-version=7.4.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>
Bump crazy-max/ghaction-github-labeler from 4.2.0 to 5.0.0
Bumps [crazy-max/ghaction-github-labeler](https://github.com/crazy-max/ghaction-github-labeler) from 4.2.0 to 5.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/crazy-max/ghaction-github-labeler/releases">crazy-max/ghaction-github-labeler's releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<ul>
<li>Node 20 as default runtime (requires <a href="https://github.com/actions/runner/releases/tag/v2.308.0">Actions Runner v2.308.0</a> or later) by <a href="https://github.com/crazy-max"><code>@​crazy-max</code></a> in <a href="https://redirect.github.com/crazy-max/ghaction-github-labeler/pull/199">crazy-max/ghaction-github-labeler#199</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/crazy-max/ghaction-github-labeler/compare/v4.2.0...v5.0.0">https://github.com/crazy-max/ghaction-github-labeler/compare/v4.2.0...v5.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/de749cf181958193cb7debf1a9c5bb28922f3e1b"><code>de749cf</code></a> Merge pull request <a href="https://redirect.github.com/crazy-max/ghaction-github-labeler/issues/198">#198</a> from crazy-max/dependabot/github_actions/actions/chec...</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/7381649272bb633d6d51bc893dbb478b64a54278"><code>7381649</code></a> Merge pull request <a href="https://redirect.github.com/crazy-max/ghaction-github-labeler/issues/199">#199</a> from crazy-max/update-node20</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/e4b824f001d7a5a367db3116dcae44cc29ef8934"><code>e4b824f</code></a> chore: node 20 as default runtime</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/f20248f4707ddc0f4cd54d026fd11fbf8cf357c5"><code>f20248f</code></a> chore: update generated content</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/2fae9f603259c9014c0275201fb7e98f5670fb4e"><code>2fae9f6</code></a> chore: update dev dependencies</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/7ce1845c0dd581c2e9b7521845fcf4579fe23f96"><code>7ce1845</code></a> chore: update to node 20</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/9230c22469d8f9d811b36a6234f72bb5b0e52a06"><code>9230c22</code></a> chore: update yarn to 3.6.3</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/f6c8f774418afb5ff5d2c47b92fe554f774c9512"><code>f6c8f77</code></a> codecov: update config</li>
<li><a href="https://github.com/crazy-max/ghaction-github-labeler/commit/3bd04ae614ce826f2ab0f68fdbaf504f481f3ce4"><code>3bd04ae</code></a> Bump actions/checkout from 3 to 4</li>
<li>See full diff in <a href="https://github.com/crazy-max/ghaction-github-labeler/compare/v4.2.0...v5.0.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crazy-max/ghaction-github-labeler&package-manager=github_actions&previous-version=4.2.0&new-version=5.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Discussion / hints:
OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting `@dependabot ignore this major version` or `@dependabot ignore this minor version`. You can also ignore all major, minor, or patch releases for a dependency by adding an [`ignore` condition](https://docs.github.com/en/code-security/supply-chain-security/configuration-options-for-dependency-updates#ignore) with the desired `update_types` to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.
Superseded by #28.
OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting `@dependabot ignore this major version` or `@dependabot ignore this minor version`. You can also ignore all major, minor, or patch releases for a dependency by adding an [`ignore` condition](https://docs.github.com/en/code-security/supply-chain-security/configuration-options-for-dependency-updates#ignore) with the desired `update_types` to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

Fix patch:
diff --git a/src/eimerdb/functions.py b/src/eimerdb/functions.py
index b9c004d..5037d35 100644
--- a/src/eimerdb/functions.py
+++ b/src/eimerdb/functions.py
@@ -16,6 +16,8 @@
 
 logger = logging.getLogger(__name__)
 
+APPLICATION_JSON = "application/json"
+
 
 def get_datetime() -> str:
     """A function that returns a datetime string.
@@ -115,9 +117,7 @@ def parse_sql_query(sql_query: str) -> dict[str, Any]:
     )
 
     update_pattern = re.compile(
-        r"""
-        ^UPDATE\s+(\w+)\s+SET\s+(.*?)\s+(?:WHERE\s+(.*))?$
-    """,
+        r"^UPDATE\s+(\w+)\s+SET\s+(.*?)\s+(?:WHERE\s+(.*))?$",
         re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE,
     )
 
@@ -187,10 +187,6 @@ def create_eimerdb(bucket_name: str, db_name: str) -> None:
     Args:
         bucket_name: A GCP bucket.
         db_name: Name of the instance.
-
-    Returns:
-        success or failure
-
     """
     creator = get_initials()
     token = AuthClient.fetch_google_credentials()
@@ -248,4 +244,3 @@ def create_eimerdb(bucket_name: str, db_name: str) -> None:
     tables_blob = bucket.blob(f"{full_path}/config/tables.json")
     tables_blob.upload_from_string(data=json.dumps({}), content_type="application/json")
     logger.info("EimerDB instance %s created.", db_name)
-    return None
diff --git a/src/eimerdb/instance.py b/src/eimerdb/instance.py
index af76832..b1ffcd5 100644
--- a/src/eimerdb/instance.py
+++ b/src/eimerdb/instance.py
@@ -26,6 +26,7 @@
 from google.cloud import storage
 from pandas import DataFrame
 
+from .functions import APPLICATION_JSON
 from .functions import arrow_schema_from_json
 from .functions import get_datetime
 from .functions import get_initials
@@ -37,6 +38,19 @@
 
 logger = logging.getLogger(__name__)
 
+ROW_ID_DEF = {"name": "row_id", "type": "string", "label": "Unique row ID"}
+PANDAS_OUTPUT_FORMAT = "pandas"
+ARROW_OUTPUT_FORMAT = "arrow"
+
+CHANGES_ALL = "all"
+CHANGES_RECENT = "recent"
+
+TABLE_PATH_KEY = "table_path"
+PARTITION_COLUMNS_KEY = "partition_columns"
+SCHEMA_KEY = "schema"
+
+SELECT_STAR_QUERY = "SELECT * FROM"
+
 
 class EimerDBInstance:
     """Represents an instance of the EimerDB database.
@@ -70,7 +84,6 @@ class EimerDBInstance:
             of a specified schema.
         query(sql_query, partition_select=None, unedited=False): Execute an SQL
             query on a table in EimerDB.
-
     """
 
     def __init__(self, bucket_name: str, eimer_name: str) -> None:
@@ -81,16 +94,8 @@ def __init__(self, bucket_name: str, eimer_name: str) -> None:
             eimer_name (str): Name of EimerDB instance.
 
         Attributes:
-            bucket (str): GCS bucket name.
-            eimerdb_name (str): EimerDB instance name.
-            path (str): Configuration path.
-            eimer_path (str): Instance path.
-            created_by (str): Creator's name.
-            time_created (str): Creation timestamp.
-            users (dict): EimerDB users and roles.
-            role_groups (dict): Role groups.
-            is_admin (bool): Admin status.
-
+            bucket_name (str): GCS bucket name.
+            eimer_name (str): EimerDB instance name.
         """
         self.bucket = bucket_name
 
@@ -133,30 +138,25 @@ def add_user(self, username: str, role: Any) -> None:
             role (Any): Role to assign (admin or user).
 
         Raises:
-            Exception: If the user is not an admin or the user already exists.
+            PermissionError: If the user is not an admin.
+            ValueError: If the user already exists.
+        """
+        if self.is_admin is not True:
+            raise PermissionError("Cannot add user. You are not an admin!")
 
-        Returns:
-            None
+        if username in self.users:
+            raise ValueError(f"User {username} already exists!")
 
-        """
-        if self.is_admin is True:
-            token = AuthClient.fetch_google_credentials()
-            client = storage.Client(credentials=token)
-            bucket = client.bucket(self.bucket)
-            users = self.users
-            new_user = {username: role}
-            if username not in users:
-                users.update(new_user)
-                user_roles_blob = bucket.blob(f"{self.eimer_path}/config/users.json")
-                user_roles_blob.upload_from_string(
-                    data=json.dumps(users), content_type="application/json"
-                )
-                print(f"User {username} added with the role {role}!")
-            else:
-                raise Exception(f"User {username} already exists!")
-        else:
-            raise Exception("Cannot add user. You are not an admin!")
-        return None
+        client = storage.Client(credentials=AuthClient.fetch_google_credentials())
+        bucket = client.bucket(self.bucket)
+
+        self.users.update({username: role})
+        user_roles_blob = bucket.blob(f"{self.eimer_path}/config/users.json")
+
+        user_roles_blob.upload_from_string(
+            data=json.dumps(self.users), content_type=APPLICATION_JSON
+        )
+        print(f"User {username} added with the role {role}!")
 
     def remove_user(self, username: str) -> None:
         """Remove a users access to the database.
@@ -165,25 +165,24 @@ def remove_user(self, username: str) -> None:
             username (str): Name of the user to remove.
 
         Raises:
-            Exception: If the user is not an admin or the user does not exist.
-
+            PermissionError: If the user is not an admin.
+            ValueError: If the user does not exist.
         """
-        if self.is_admin is True:
-            token = AuthClient.fetch_google_credentials()
-            client = storage.Client(credentials=token)
-            bucket = client.bucket(self.bucket)
-            users = self.users
-            if username in users:
-                del users[username]
-                user_roles_blob = bucket.blob(f"{self.eimer_path}/config/users.json")
-                user_roles_blob.upload_from_string(
-                    data=json.dumps(users), content_type="application/json"
-                )
-                print(f"User {username} successfully removed!")
-            else:
-                print(f"The user {username} does not exist.")
-        else:
-            raise Exception("Cannot remove user. You are not an admin!")
+        if self.is_admin is not True:
+            raise PermissionError("Cannot remove user. You are not an admin!")
+
+        if username not in self.users:
+            raise ValueError(f"User {username} does not exist.")
+
+        client = storage.Client(credentials=AuthClient.fetch_google_credentials())
+        bucket = client.bucket(self.bucket)
+
+        del self.users[username]
+        user_roles_blob = bucket.blob(f"{self.eimer_path}/config/users.json")
+        user_roles_blob.upload_from_string(
+            data=json.dumps(self.users), content_type=APPLICATION_JSON
+        )
+        print(f"User {username} successfully removed!")
 
     def create_table(
         self,
@@ -201,108 +200,149 @@ def create_table(
             editable (bool, optional): Indicates if the table is editable.
 
         Raises:
-            Exception: If the current user is not an admin.
-
+            PermissionError: If the current user is not an admin.
         """
-        if self.is_admin is True:
-            token = AuthClient.fetch_google_credentials()
-            client = storage.Client(credentials=token)
-            row_id_def = {"name": "row_id", "type": "string", "label": "Unique row ID"}
-            schema.insert(0, row_id_def)
-            arrow_schema = arrow_schema_from_json(schema)
-            tables = self.tables
-            bucket = client.bucket(self.bucket)
-            initials = get_initials()
-            new_table = {
-                table_name: {
-                    "created_by": initials,
-                    "table_path": f"{self.eimer_path}/{table_name}",
-                    "bucket": self.bucket,
-                    "editable": editable,
-                    "schema": arrow_schema,
-                }
+        if self.is_admin is not True:
+            raise PermissionError("Cannot create table. You are not an admin!")
+
+        schema.insert(0, ROW_ID_DEF)
+
+        new_table = {
+            table_name: {
+                "created_by": get_initials(),
+                TABLE_PATH_KEY: f"{self.eimer_path}/{table_name}",
+                "bucket": self.bucket,
+                "editable": editable,
+                SCHEMA_KEY: schema,
+                PARTITION_COLUMNS_KEY: partition_columns,
             }
-            if partition_columns:
-                new_table[table_name]["partition_columns"] = partition_columns
-            else:
-                new_table[table_name]["partition_columns"] = None
-            tables.update(new_table)
-            tables_blob = bucket.blob(f"{self.eimer_path}/config/tables.json")
-            tables_blob.upload_from_string(
-                data=json.dumps(tables), content_type="application/json"
-            )
-        else:
-            raise Exception("Cannot create table. You are not an admin!")
+        }
+        self.tables.update(new_table)
+
+        token = AuthClient.fetch_google_credentials()
+        bucket = storage.Client(credentials=token).bucket(self.bucket)
+
+        tables_blob = bucket.blob(f"{self.eimer_path}/config/tables.json")
+        tables_blob.upload_from_string(
+            data=json.dumps(self.tables), content_type=APPLICATION_JSON
+        )
 
     def insert(self, table_name: str, df: pd.DataFrame) -> None:
         """Insert unedited data into a main table.
 
         Args:
             table_name (str): Name of the table to insert data into.
-            df (pandas.DataFrame): DataFrame containing the data to insert.
+            df (pandas.DataFrame): DataFrame containing the data to insert
 
         Raises:
-            Exception: If the current user is not an admin.
-
+            PermissionError: If the current user is not an admin.
         """
-        if self.is_admin is True:
-            json_data = self.tables[table_name]
-            arrow_schema = arrow_schema_from_json(json_data["schema"])
-            df["row_id"] = df.apply(lambda row: uuid4(), axis=1)
-            df["row_id"] = df["row_id"].astype(str)
-            table = pa.Table.from_pandas(df, schema=arrow_schema)
-
-            df_raw = df.copy()
-            df_raw["user"] = get_initials()
-            df_raw["datetime"] = get_datetime()
-            df_raw["operation"] = "insert"
-
-            arrow_schema_raw = arrow_schema
-            arrow_schema_raw = arrow_schema_raw.append(pa.field("user", pa.string()))
-            arrow_schema_raw = arrow_schema_raw.append(
-                pa.field("datetime", pa.string())
-            )
-            arrow_schema_raw = arrow_schema_raw.append(
-                pa.field("operation", pa.string())
+        if self.is_admin is not True:
+            raise PermissionError(
+                "Cannot insert into main table. You are not an admin!"
             )
 
-            table_raw = pa.Table.from_pandas(df_raw, schema=arrow_schema_raw)
-            timestamp_column = table_raw["datetime"].cast(pa.timestamp("ns"))
+        df["row_id"] = df.apply(lambda row: str(uuid4()), axis=1)
 
-            table_raw = table_raw.drop(["datetime"])
+        json_data = self.tables[table_name]
+        arrow_schema = arrow_schema_from_json(json_data["schema"])
+        table = pa.Table.from_pandas(df, schema=arrow_schema)
 
-            table_raw = table_raw.add_column(
-                len(table_raw.column_names),
-                pa.field("datetime", pa.timestamp("ns")),
-                timestamp_column,
-            )
+        df_raw = df.copy()
+        df_raw["user"] = get_initials()
+        df_raw["datetime"] = get_datetime()
+        df_raw["operation"] = "insert"
 
-            insert_id = uuid4()
-            table_path = json_data["table_path"]
-            partitions = json_data["partition_columns"]
-            filename = f"insert_{insert_id}_{{i}}.parquet"
+        arrow_schema_raw = arrow_schema
+        arrow_schema_raw = arrow_schema_raw.append(pa.field("user", pa.string()))
+        arrow_schema_raw = arrow_schema_raw.append(pa.field("datetime", pa.string()))
+        arrow_schema_raw = arrow_schema_raw.append(pa.field("operation", pa.string()))
 
-            fs = FileClient.get_gcs_file_system()
+        table_raw = pa.Table.from_pandas(df_raw, schema=arrow_schema_raw)
+        timestamp_column = table_raw["datetime"].cast(pa.timestamp("ns"))
 
-            # noinspection PyTypeChecker
-            pq.write_to_dataset(
-                table,
-                root_path=f"gs://{self.bucket}/{table_path}",
-                partition_cols=partitions,
-                basename_template=filename,
-                filesystem=fs,
-                schema=arrow_schema,
-            )
-            pq.write_to_dataset(
-                table_raw,
-                root_path=f"gs://{self.bucket}/{table_path}_raw",
-                partition_cols=partitions,
-                basename_template=filename,
-                filesystem=fs,
-            )
-            print("Data successfully inserted!")
-        else:
-            raise Exception("Cannot insert into main table. You are not an admin!")
+        table_raw = table_raw.drop(["datetime"])
+
+        table_raw = table_raw.add_column(
+            len(table_raw.column_names),
+            pa.field("datetime", pa.timestamp("ns")),
+            timestamp_column,
+        )
+
+        insert_id = uuid4()
+        table_path = json_data["table_path"]
+        partitions = json_data["partition_columns"]
+        filename = f"insert_{insert_id}_{{i}}.parquet"
+
+        fs = FileClient.get_gcs_file_system()
+
+        # noinspection PyTypeChecker
+        pq.write_to_dataset(
+            table=table,
+            root_path=f"gs://{self.bucket}/{table_path}",
+            partition_cols=partitions,
+            basename_template=filename,
+            filesystem=fs,
+            schema=arrow_schema,
+        )
+        # noinspection PyTypeChecker
+        pq.write_to_dataset(
+            table=table_raw,
+            root_path=f"gs://{self.bucket}/{table_path}_raw",
+            partition_cols=partitions,
+            basename_template=filename,
+            filesystem=fs,
+        )
+        print("Data successfully inserted!")
+
+    def get_changes(self, table_name: str) -> DataFrame:
+        """Retrieve changes for a given table.
+
+        Args:
+            table_name (str): The name of the table for which changes are to be retrieved.
+
+        Returns:
+            DataFrame: A pandas DataFrame containing the changes for the specified table.
+        """
+        path = self.tables[table_name][TABLE_PATH_KEY]
+
+        fs = FileClient.get_gcs_file_system()
+        # noinspection PyTypeChecker
+        dataset = ds.dataset(
+            f"{self.bucket}/{path}_changes/",
+            format="parquet",
+            partitioning="hive",
+            filesystem=fs,
+        )
+
+        df_changes: DataFrame = dataset.to_table().to_pandas()
+        return df_changes
+
+    def combine_changes(self, table_name: str) -> None:
+        """Combines the files containing the changes of the table into one file.
+
+        Args:
+            table_name (str): The name of the table for which changes are to be merged.
+        """
+        client = storage.Client(credentials=AuthClient.fetch_google_credentials())
+        bucket = client.bucket(self.bucket)
+
+        partitions = self.tables[table_name][PARTITION_COLUMNS_KEY]
+        source_folder = self.tables[table_name][TABLE_PATH_KEY] + "_changes"
+        blobs_to_delete = list(bucket.list_blobs(prefix=source_folder))
+
+        # noinspection PyTypeChecker
+        pq.write_to_dataset(
+            table=pa.Table.from_pandas(self.get_changes(table_name)),
+            root_path=f"gs://{self.bucket}/{source_folder}",
+            partition_cols=partitions,
+            basename_template=f"merged_commit_{uuid4()}_{{i}}.parquet",
+            filesystem=FileClient.get_gcs_file_system(),
+        )
+        for blob in blobs_to_delete:
+            blob.delete()
+
+        print("The changes were successfully merged into one file per partition!")
 
     def query(
         self,
@@ -328,7 +368,7 @@ def query(
         """
         instance_name = self.eimerdb_name
         if output_format is None:
-            output_format = "pandas"
+            output_format = PANDAS_OUTPUT_FORMAT
 
         parsed_query = parse_sql_query(sql_query)
         try:
@@ -362,9 +402,9 @@ def query(
                 df_changes = None
 
                 if editable is True and unedited is False:
-                    slct_qry = "SELECT * FROM"
+                    select_query = SELECT_STAR_QUERY
                     df_changes = self.query_changes(
-                        f"{slct_qry} {table_name}",
+                        f"{select_query} {table_name}",
                         partition_select,
                         output_format="arrow",
                         changes_output="recent",
@@ -376,9 +416,9 @@ def query(
                 con.register(table_name, df)
                 del df
 
-            if output_format == "pandas":
+            if output_format == PANDAS_OUTPUT_FORMAT:
                 output = con.execute(sql_query).df()
-            elif output_format == "arrow":
+            elif output_format == ARROW_OUTPUT_FORMAT:
                 output = con.execute(sql_query).arrow()
             else:
                 raise ValueError(f"Invalid output format: {output_format}")
@@ -393,25 +433,24 @@ def query(
                 raise Exception(f"The table {table_name} is not editable!")
             try:
                 columns = parsed_query["columns"]
-            except Exception:
+            except ValueError:
                 columns = None
             if columns == ["*"]:
                 columns = None
             json_data = self.tables[table_name]
             table_name = parsed_query["table_name"]
-            arrow_schema = arrow_schema_from_json(json_data["schema"])
+            arrow_schema = arrow_schema_from_json(json_data[SCHEMA_KEY])
             where_clause = parsed_query["where_clause"]
             table_config = self.tables[table_name]
-            partitions = table_config["partition_columns"]
+            partitions = table_config[PARTITION_COLUMNS_KEY]
 
             arrow_schema = arrow_schema.append(pa.field("user", pa.string()))
             arrow_schema = arrow_schema.append(pa.field("datetime", pa.string()))
             arrow_schema = arrow_schema.append(pa.field("operation", pa.string()))
 
-            slct_qry = "SELECT * FROM"
-
             df_update_results: pd.DataFrame = self.query(
-                f"{slct_qry} {table_name} WHERE {where_clause}", partition_select
+                f"{SELECT_STAR_QUERY} {table_name} WHERE {where_clause}",
+                partition_select,
             )
 
             df_update_results["user"] = get_initials()
@@ -427,7 +466,7 @@ def query(
 
             df_updates_len = len(df_updates_commits)
 
-            table_path = self.tables[table_name]["table_path"] + "_changes"
+            table_path = self.tables[table_name][TABLE_PATH_KEY] + "_changes"
             fs = FileClient.get_gcs_file_system()
 
             update_table = pa.Table.from_pandas(df_updates_commits, schema=arrow_schema)
@@ -435,6 +474,7 @@ def query(
             unique_file_id = uuid4()
             filename = f"commit_{unique_file_id}_{{i}}.parquet"
 
+            # noinspection PyTypeChecker
             pq.write_to_dataset(
                 update_table,
                 root_path=f"gs://{self.bucket}/{table_path}",
@@ -449,29 +489,28 @@ def query(
             table_config = self.tables[table_name]
             editable = table_config["editable"]
             if editable is False:
-                raise Exception(f"The table {table_name} is not editable!")
+                raise ValueError(f"The table {table_name} is not editable!")
             try:
                 columns = parsed_query["columns"]
-            except Exception:
+            except ValueError:
                 columns = None
             if columns == ["*"]:
                 columns = None
             json_data = self.tables[table_name]
             table_name = parsed_query["table_name"]
-            arrow_schema = arrow_schema_from_json(json_data["schema"])
+            arrow_schema = arrow_schema_from_json(json_data[SCHEMA_KEY])
             where_clause = parsed_query["where_clause"]
             instance_name = self.eimerdb_name
             table_config = self.tables[table_name]
-            partitions = table_config["partition_columns"]
+            partitions = table_config[PARTITION_COLUMNS_KEY]
 
             arrow_schema = arrow_schema.append(pa.field("user", pa.string()))
             arrow_schema = arrow_schema.append(pa.field("datetime", pa.string()))
             arrow_schema = arrow_schema.append(pa.field("operation", pa.string()))
 
-            slct_qry = "SELECT * FROM"
-
             df_delete_results: pd.DataFrame = self.query(
-                f"{slct_qry} {table_name} WHERE {where_clause}", partition_select
+                f"{SELECT_STAR_QUERY} {table_name} WHERE {where_clause}",
+                partition_select,
             )
 
             df_delete_results["user"] = get_initials()
@@ -486,16 +525,17 @@ def query(
 
             df_deletions_len = len(df_deletions)
 
-            table_path = self.tables[table_name]["table_path"] + "_changes"
+            table_path = self.tables[table_name][TABLE_PATH_KEY] + "_changes"
             fs = FileClient.get_gcs_file_system()
 
-            deletion_Table = pa.Table.from_pandas(df_deletions, schema=arrow_schema)
+            deletion_table = pa.Table.from_pandas(df_deletions, schema=arrow_schema)
 
             unique_file_id = uuid4()
             filename = f"commit_{unique_file_id}_{{i}}.parquet"
 
+            # noinspection PyTypeChecker
             pq.write_to_dataset(
-                deletion_Table,
+                deletion_table,
                 root_path=f"gs://{self.bucket}/{table_path}",
                 partition_cols=partitions,
                 basename_template=filename,
@@ -509,199 +549,143 @@ def query_changes(
         self,
         sql_query: str,
         partition_select: Optional[dict[str, Any]] = None,
-        unedited: Optional[bool] = False,
-        output_format: Optional[str] = None,
-        changes_output: Optional[str] = "all",
-    ) -> Union[pd.DataFrame, pa.Table, None]:
+        unedited: bool = False,
+        output_format: str = PANDAS_OUTPUT_FORMAT,
+        changes_output: str = CHANGES_ALL,
+    ) -> Optional[Union[pd.DataFrame, pa.Table]]:
         """Query changes made in the database table.
 
         Args:
             sql_query (str): The SQL query to execute.
             partition_select (Dict, optional):
                 Dictionary containing partition selection criteria. Defaults to None.
-            unedited (bool, optional):
+            unedited (bool):
                 Flag indicating whether to retrieve unedited changes. Defaults to False.
-            output_format (str, optional):
+            output_format (str):
                 The desired output format ('pandas' or 'arrow'). Defaults to 'pandas'.
-            changes_output (str, optional):
+            changes_output (str):
                 The changes that are to be retrieved ('recent' or 'all'). Defaults to 'all'.
 
         Returns:
-            Union[pd.DataFrame, pa.Table, str]:
+            Optional[pd.DataFrame, pa.Table]:
                 Returns a pandas DataFrame if 'pandas' output format is specified,
                 an arrow Table if 'arrow' output format is specified,
+                or None if operation is different from SELECT.
+
+        Raises:
+            ValueError: If the output format is invalid.
         """
-        if output_format is None:
-            output_format = "pandas"
+        if output_format not in (PANDAS_OUTPUT_FORMAT, ARROW_OUTPUT_FORMAT):
+            raise ValueError(f"Invalid output format: {output_format}")
 
         parsed_query = parse_sql_query(sql_query)
+        if parsed_query["operation"] != "SELECT":
+            return None
+
         table_name = parsed_query["table_name"][0]
+
         table_config = self.tables[table_name]
         editable = table_config["editable"]
         instance_name = self.eimerdb_name
-        table_schema = arrow_schema_from_json(self.tables[table_name]["schema"])
+        table_schema = arrow_schema_from_json(self.tables[table_name][SCHEMA_KEY])
 
         df: Optional[Any] = None
         df_changes: Optional[Any] = None
 
-        if parsed_query["operation"] == "SELECT":
-            try:
-                columns = parsed_query["columns"]
-            except Exception:
-                columns = None
-            if columns == ["*"]:
-                columns = None
-            partitions = table_config["partition_columns"]
-            bucket_name = table_config["bucket"]
-            partitions_len = len(partitions)
-            partition_levels = "**/" * partitions_len + "*"
-            fs = FileClient.get_gcs_file_system()
-            table_name_changes = table_name + "_changes"
-            table_files_changes = fs.glob(
-                f"gs://{bucket_name}/eimerdb/{instance_name}/{table_name_changes}/{partition_levels}"
-            )
+        try:
+            columns = parsed_query["columns"]
+        except Exception:
+            columns = None
+        if columns == ["*"]:
+            columns = None
+        partitions = table_config["partition_columns"]
+        bucket_name = table_config["bucket"]
+        partitions_len = len(partitions) if partitions is not None else 0
+        partition_levels = "**/" * partitions_len + "*"
+        fs = FileClient.get_gcs_file_system()
+        table_name_changes = table_name + "_changes"
+        table_files_changes = fs.glob(
+            f"gs://{bucket_name}/eimerdb/{instance_name}/{table_name_changes}/{partition_levels}"
+        )
 
-            max_depth = 0
-            try:
-                max_depth = max(obj.count("/") for obj in table_files_changes)
-                no_changes = False
-            except ValueError:
-                no_changes = True
+        max_depth = 0
+        try:
+            max_depth = max(obj.count("/") for obj in table_files_changes)
+            no_changes = False
+        except ValueError:
+            no_changes = True
+            if output_format == "pandas":
+                df_changes = pd.DataFrame()
+            elif output_format == "arrow":
+                df_changes = pa.table(pd.DataFrame())
+
+        if no_changes is not True:
+            table_files_changes = [
+                obj for obj in table_files_changes if obj.count("/") == max_depth
+            ]
+            if partition_select is not None:
+                table_files_changes = filter_partitions(
+                    table_files_changes,
+                    partition_select,
+                )
+
+            dataset: pa.Table = pq.read_table(table_files_changes, filesystem=fs)
+            sql_query = sql_query.replace(f"FROM {table_name}", "FROM dataset")
+            if dataset.num_rows != 0:
+                con = duckdb.connect()
+                if output_format == "pandas":
+                    df_changes = con.execute(sql_query).df()
+                elif output_format == "arrow":
+                    df_changes = con.execute(sql_query).arrow()
+
+            elif dataset.num_rows == 0:
                 if output_format == "pandas":
                     df_changes = pd.DataFrame()
                 elif output_format == "arrow":
                     df_changes = pa.table(pd.DataFrame())
 
-            if no_changes is not True:
-                table_files_changes = [
-                    obj for obj in table_files_changes if obj.count("/") == max_depth
-                ]
-                if partition_select is not None:
-                    table_files_changes = filter_partitions(
-                        table_files_changes,
-                        partition_select,
-                    )
+        table_name_changes_all = table_name + "_changes_all"
+        table_files_changes_all = fs.glob(
+            f"gs://{bucket_name}/eimerdb/{instance_name}/{table_name_changes_all}/{partition_levels}"
+        )
+        try:
+            max_depth = max(obj.count("/") for obj in table_files_changes_all)
+            no_changes_all = False
+        except ValueError:
+            no_changes_all = True
+
+        if no_changes_all is not True:
+            table_files_changes_all = [
+                obj for obj in table_files_changes_all if obj.count("/") == max_depth
+            ]
+            if partition_select is not None:
+                table_files_changes_all = filter_partitions(
+                    table_files_changes_all, partition_select
+                )
 
-                dataset: pa.Table = pq.read_table(table_files_changes, filesystem=fs)
-                sql_query = sql_query.replace(f"FROM {table_name}", "FROM dataset")
-                if dataset.num_rows != 0:
-                    con = duckdb.connect()
-                    if output_format == "pandas":
-                        df_changes = con.execute(sql_query).df()
-                    elif output_format == "arrow":
-                        df_changes = con.execute(sql_query).arrow()
-
-                elif dataset.num_rows == 0:
-                    if output_format == "pandas":
-                        df_changes = pd.DataFrame()
-                    elif output_format == "arrow":
-                        df_changes = pa.table(pd.DataFrame())
-
-            table_name_changes_all = table_name + "_changes_all"
-            table_files_changes_all = fs.glob(
-                f"gs://{bucket_name}/eimerdb/{instance_name}/{table_name_changes_all}/{partition_levels}"
+            dataset = pq.read_table(
+                table_files_changes_all, filesystem=fs, columns=columns
             )
-            try:
-                max_depth = max(obj.count("/") for obj in table_files_changes_all)
-                no_changes_all = False
-            except ValueError:
-                no_changes_all = True
-
-            if no_changes_all is not True:
-                table_files_changes_all = [
-                    obj
-                    for obj in table_files_changes_all
-                    if obj.count("/") == max_depth
-                ]
-                if partition_select is not None:
-                    table_files_changes_all = filter_partitions(
-                        table_files_changes_all, partition_select
-                    )
 
-                dataset = pq.read_table(
-                    table_files_changes_all, filesystem=fs, columns=columns
-                )
+            sql_query = sql_query.replace(f"FROM {table_name}", "FROM dataset")
+            if columns is not None and editable is True and unedited is False:
+                sql_query = sql_query.replace(" FROM", ", row_id FROM")
+            if dataset.num_rows != 0:
+                con = duckdb.connect()
+                if output_format == "pandas":
+                    df_changes_all = con.execute(sql_query).df()
+                    df = pd.concat([df_changes_all, df_changes])
+                elif output_format == "arrow":
+                    df_changes_all = con.execute(sql_query).arrow()
+                    df = pa.concat_tables([df_changes_all, df_changes])
+                    df = df.cast(table_schema)
 
-                sql_query = sql_query.replace(f"FROM {table_name}", "FROM dataset")
-                if columns is not None and editable is True and unedited is False:
-                    sql_query = sql_query.replace(" FROM", ", row_id FROM")
-                if dataset.num_rows != 0:
-                    con = duckdb.connect()
-                    if output_format == "pandas":
-                        df_changes_all = con.execute(sql_query).df()
-                        df = pd.concat([df_changes_all, df_changes])
-                    elif output_format == "arrow":
-                        df_changes_all = con.execute(sql_query).arrow()
-                        df = pa.concat_tables([df_changes_all, df_changes])
-                        df = df.cast(table_schema)
-
-            if changes_output == "all":
-                if no_changes_all is not True:
-                    return df
-                elif no_changes_all is True:
-                    return df_changes
-            elif changes_output == "recent":
+        if changes_output == "all":
+            if no_changes_all is not True:
+                return df
+            elif no_changes_all is True:
                 return df_changes
-            else:
-                return None
+        elif changes_output == "recent":
+            return df_changes
         else:
             return None
-
-    def get_changes(self, table_name: str) -> DataFrame:
-        """Retrieve changes for a given table.
-
-        Args:
-            table_name (str): The name of the table for which changes are to be retrieved.
-
-        Returns:
-            DataFrame: A pandas DataFrame containing the changes for the specified table.
-        """
-        fs = FileClient.get_gcs_file_system()
-        bucket = self.bucket
-        path = self.tables[table_name]["table_path"]
-        dataset = ds.dataset(
-            f"{bucket}/{path}_changes/",
-            format="parquet",
-            partitioning="hive",
-            filesystem=fs,
-        )
-        df_changes: DataFrame = dataset.to_table().to_pandas()
-        return df_changes
-
-    def combine_changes(self, table_name: str) -> None:
-        """Combines the files containing the changes of the table into one file.
-
-        Args:
-            table_name (str): The name of the table for which changes are to be merged.
-
-        Returns:
-            None
-        """
-        fs = FileClient.get_gcs_file_system()
-        df_changes = self.get_changes(table_name)
-
-        token = AuthClient.fetch_google_credentials()
-        client = storage.Client(credentials=token)
-        bucket = client.bucket(self.bucket)
-        path = self.tables[table_name]["table_path"]
-        partitions = self.tables[table_name]["partition_columns"]
-        source_folder = path + "_changes"
-        blobs_to_delete = list(bucket.list_blobs(prefix=source_folder))
-        filename = f"merged_commit_{uuid4()}_{{i}}.parquet"
-        table = pa.Table.from_pandas(df_changes)
-        pq.write_to_dataset(
-            table,
-            root_path=f"gs://{self.bucket}/{source_folder}",
-            partition_cols=partitions,
-            basename_template=filename,
-            filesystem=fs,
-        )
-        for blob in blobs_to_delete:
-            blob.delete()
-
-        logging.info(
-            "The changes were successfully combined into one file per partition!"
-        )
-
-        print("The changes were successfully combined into one file per partition!")
-        return None
diff --git a/src/eimerdb/query.py b/src/eimerdb/query.py
index 9b62fdc..38e79c2 100644
--- a/src/eimerdb/query.py
+++ b/src/eimerdb/query.py
@@ -107,7 +107,6 @@ def update_pyarrow_table(df: pa.Table, df_changes: pa.Table) -> pa.Table:
     )
 
     row_id_max = df_changes.group_by("row_id").aggregate([("datetime", "max")])
-
     new_names = ["row_id", "datetime"]
     row_id_max = row_id_max.select(["row_id", "datetime_max"])
 


Test patch:
diff --git a/tests/test_functions.py b/tests/test_functions.py
index fa3e14a..87d584d 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -1,23 +1,87 @@
-from datetime import datetime
+import unittest
+from unittest.mock import MagicMock
 from unittest.mock import Mock
 from unittest.mock import patch
 
+import pyarrow as pa
+
+from eimerdb.functions import arrow_schema_from_json
+from eimerdb.functions import create_eimerdb
 from eimerdb.functions import get_datetime
 from eimerdb.functions import get_initials
+from eimerdb.functions import get_json
+from eimerdb.functions import parse_sql_query
+
+
+class TestFunctions(unittest.TestCase):
+
+    def test_get_datetime(self) -> None:
+        result = get_datetime()
+        self.assertTrue(isinstance(result, str))
+        # Check if the string matches the expected format
+        self.assertRegex(result, r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d+")
+
+    @patch("eimerdb.functions.AuthClient.fetch_local_user_from_jupyter")
+    def test_get_initials_with_mock(self, mock_fetch: Mock) -> None:
+        mock_fetch.return_value = {"username": "john.doe@example.com"}
+        result = get_initials()
+        self.assertEqual(result, "john.doe")
+
+    def test_get_initials_without_mock(self) -> None:
+        result = get_initials()
+        self.assertEqual(result, "user")
+
+    @patch(
+        "eimerdb.functions.AuthClient.fetch_google_credentials", return_value="token"
+    )
+    @patch("google.cloud.storage.Client")
+    def test_get_json(self, mock_client: Mock, _: Mock) -> None:
+        mock_blob = MagicMock()
+        mock_blob.download_as_text.return_value = '{"key": "value"}'
+
+        mock_bucket = MagicMock()
+        mock_bucket.blob.return_value = mock_blob
 
+        mock_client.return_value.get_bucket.return_value = mock_bucket
 
-def test_get_datetime() -> None:
-    assert get_datetime().startswith(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+        result = get_json("bucket_name", "blob_path")
+        self.assertEqual(result, {"key": "value"})
 
+    def test_arrow_schema_from_json(self) -> None:
+        json_schema = [
+            {"name": "field1", "type": "int8", "label": "Field 1"},
+            {"name": "field2", "type": "string", "label": "Field 2"},
+            {"name": "field3", "type": "timestamp(us)", "label": "Field 3"},
+        ]
+        result = arrow_schema_from_json(json_schema)
 
-@patch("eimerdb.functions.AuthClient.fetch_local_user_from_jupyter")
-def test_get_initials_given_mocked_auth_client(auth_client_mock: Mock) -> None:
-    auth_client_mock.return_value = {"username": "user42@some.org"}
+        assert result == pa.schema(
+            [
+                pa.field("field1", pa.int8(), metadata={"label": "Field 1"}),
+                pa.field("field2", pa.string(), metadata={"label": "Field 2"}),
+                pa.field("field3", pa.timestamp("us"), metadata={"label": "Field 3"}),
+            ]
+        )
 
-    assert get_initials() == "user42"
+    def test_parse_sql_query(self) -> None:
+        sql_query = "SELECT * FROM table WHERE condition"
+        result = parse_sql_query(sql_query)
+        assert result == {
+            "columns": ["*"],
+            "operation": "SELECT",
+            "select_clause": "",
+            "table_name": ["table"],
+            "where_clause": "condition",
+        }
 
-    auth_client_mock.assert_called_once()
+    @patch("eimerdb.functions.AuthClient.fetch_google_credentials")
+    @patch("google.cloud.storage.Client")
+    def test_create_eimerdb(self, mock_client: Mock, mock_fetch: Mock) -> None:
+        mock_fetch.return_value = "token"
+        mock_blob = MagicMock()
+        mock_bucket = MagicMock()
 
+        mock_bucket.blob.return_value = mock_blob
+        mock_client.return_value.bucket.return_value = mock_bucket
 
-def test_get_initials_given_no_auth_client() -> None:
-    assert get_initials() == "user"
+        create_eimerdb("bucket_name", "db_name")
diff --git a/tests/test_instance_admin_user.py b/tests/test_instance_admin_user.py
new file mode 100644
index 0000000..d0a6801
--- /dev/null
+++ b/tests/test_instance_admin_user.py
@@ -0,0 +1,299 @@
+import unittest
+from unittest.mock import ANY
+from unittest.mock import Mock
+from unittest.mock import call
+from unittest.mock import patch
+
+import pandas as pd
+import pyarrow as pa
+import pyarrow.dataset as ds
+from google.cloud.storage import Blob
+
+from eimerdb.instance import EimerDBInstance
+
+
+class TestEimerDBInstanceAdminUser(unittest.TestCase):
+    @patch("eimerdb.instance.get_json")
+    @patch("eimerdb.instance.get_initials", return_value="admin_user")
+    def setUp(self, _: Mock, mock_get_json: Mock) -> None:
+        mock_get_json.side_effect = [
+            {
+                "eimerdb_name": "test_eimerdb",
+                "path": "path/to/config",
+                "eimer_path": "path/to/eimer",
+                "created_by": "admin_user",
+                "time_created": "2024-03-09T12:00:00Z",
+            },
+            {"admin_user": "admin"},
+            {"admin_user": {"admin_group": ["admin_user"]}},
+            {
+                "table1": {
+                    "bucket": "test_bucket",
+                    "created_by": "admin_user",
+                    "editable": True,
+                    "partition_columns": None,
+                    "schema": [
+                        {"label": "Unique row ID", "name": "row_id", "type": "string"},
+                        {"label": "Field 1", "name": "field1", "type": "int8"},
+                    ],
+                    "table_path": "path/to/eimer/table1",
+                },
+            },
+        ]
+
+        self.instance = EimerDBInstance("test_bucket", "test_eimer")
+
+    def test_init_admin(self) -> None:
+        self.assertEqual(self.instance.bucket, "test_bucket")
+        self.assertEqual(self.instance.eimerdb_name, "test_eimerdb")
+        self.assertEqual(self.instance.path, "path/to/config")
+        self.assertEqual(self.instance.eimer_path, "path/to/eimer")
+        self.assertEqual(self.instance.created_by, "admin_user")
+        self.assertEqual(self.instance.time_created, "2024-03-09T12:00:00Z")
+        self.assertEqual(self.instance.users, {"admin_user": "admin"})
+        self.assertEqual(
+            self.instance.role_groups, {"admin_user": {"admin_group": ["admin_user"]}}
+        )
+        self.assertEqual(True, self.instance.is_admin)
+
+    @patch("eimerdb.instance.AuthClient.fetch_google_credentials")
+    @patch("eimerdb.instance.storage.Client")
+    def test_add_user_success(
+        self, mock_storage_client: Mock, mock_fetch_credentials: Mock
+    ) -> None:
+        # Setup
+        mock_credentials = Mock()
+        mock_fetch_credentials.return_value = mock_credentials
+        mock_bucket = mock_storage_client.return_value.bucket.return_value
+        mock_blob = mock_bucket.blob.return_value
+
+        # Test
+        self.instance.add_user("new_user", "user")
+
+        # Assertions
+        mock_fetch_credentials.assert_called_once()
+        mock_storage_client.assert_called_once_with(credentials=mock_credentials)
+        mock_bucket.blob.assert_called_once_with("path/to/eimer/config/users.json")
+        mock_blob.upload_from_string.assert_called_once()
+
+    def test_add_user_user_exists(self) -> None:
+        # Test & Assertion
+        with self.assertRaises(ValueError) as context:
+            self.instance.add_user("admin_user", "admin")
+        self.assertEqual("User admin_user already exists!", str(context.exception))
+
+    @patch("eimerdb.instance.AuthClient.fetch_google_credentials")
+    @patch("eimerdb.instance.storage.Client")
+    def test_remove_user_success(
+        self, mock_storage_client: Mock, mock_fetch_credentials: Mock
+    ) -> None:
+        # Setup
+        mock_credentials = Mock()
+        mock_fetch_credentials.return_value = mock_credentials
+        mock_bucket = mock_storage_client.return_value.bucket.return_value
+        mock_blob = mock_bucket.blob.return_value
+
+        # Test
+        self.instance.remove_user("admin_user")
+
+        # Assertions
+        mock_fetch_credentials.assert_called_once()
+        mock_storage_client.assert_called_once_with(credentials=mock_credentials)
+        mock_bucket.blob.assert_called_once_with("path/to/eimer/config/users.json")
+        mock_blob.upload_from_string.assert_called_once()
+
+    def test_remove_user_user_not_exists(self) -> None:
+        # Test & Assertion
+        with self.assertRaises(ValueError) as context:
+            self.instance.remove_user("new_user")
+        self.assertEqual("User new_user does not exist.", str(context.exception))
+
+    @patch("eimerdb.instance.AuthClient.fetch_google_credentials")
+    @patch("eimerdb.instance.storage.Client")
+    def test_create_table_admin(
+        self, mock_storage_client: Mock, mock_fetch_credentials: Mock
+    ) -> None:
+        # Setup
+        mock_credentials = Mock()
+        mock_fetch_credentials.return_value = mock_credentials
+
+        mock_bucket = mock_storage_client.return_value.bucket.return_value
+        mock_blob = mock_bucket.blob.return_value
+
+        table_name = "table2"
+        schema = [{"name": "field1", "type": "int8", "label": "Field 1"}]
+
+        # Test
+        self.instance.create_table(table_name, schema)
+
+        # Assertions
+        self.assertEqual(
+            self.instance.tables,
+            {
+                "table1": {
+                    "bucket": "test_bucket",
+                    "created_by": "admin_user",
+                    "editable": True,
+                    "partition_columns": None,
+                    "schema": [
+                        {"label": "Unique row ID", "name": "row_id", "type": "string"},
+                        {"label": "Field 1", "name": "field1", "type": "int8"},
+                    ],
+                    "table_path": "path/to/eimer/table1",
+                },
+                table_name: {
+                    "bucket": "test_bucket",
+                    "created_by": "user",
+                    "editable": True,
+                    "partition_columns": None,
+                    "schema": [
+                        {"label": "Unique row ID", "name": "row_id", "type": "string"},
+                        {"label": "Field 1", "name": "field1", "type": "int8"},
+                    ],
+                    "table_path": "path/to/eimer/table2",
+                },
+            },
+        )
+
+        # Mock assertions
+        mock_fetch_credentials.assert_called_once()
+        mock_storage_client.assert_called_once_with(credentials=mock_credentials)
+        mock_bucket.blob.assert_called_once_with("path/to/eimer/config/tables.json")
+        mock_blob.upload_from_string.assert_called_once()
+
+    @patch("eimerdb.instance.FileClient.get_gcs_file_system")
+    def test_main_table_insert(self, mock_get_gcs_file_system: Mock) -> None:
+        mock_fs = mock_get_gcs_file_system.return_value
+
+        # Sample DataFrame for testing
+        df = pd.DataFrame({"row_id": ["1", "2", "2"], "field1": [1, 2, 3]})
+
+        expected_calls = [
+            call(
+                table=ANY,
+                root_path="gs://test_bucket/path/to/eimer/table1",
+                partition_cols=None,
+                basename_template=ANY,
+                filesystem=mock_fs,
+                schema=ANY,
+            ),
+            call(
+                table=ANY,
+                root_path="gs://test_bucket/path/to/eimer/table1_raw",
+                partition_cols=None,
+                basename_template=ANY,
+                filesystem=mock_fs,
+            ),
+        ]
+        with patch("eimerdb.instance.pq.write_to_dataset") as mock_write_to_dataset:
+            # Call the method under test
+            self.instance.insert(table_name="table1", df=df)
+            mock_write_to_dataset.assert_has_calls(expected_calls)
+
+    @patch("eimerdb.instance.FileClient.get_gcs_file_system")
+    @patch("eimerdb.instance.ds.dataset")
+    def test_get_changes(self, mock_dataset: Mock, _: Mock) -> None:
+        expected_df = pd.DataFrame([{"row_id": "1", "field1": 1}])
+
+        dataset = Mock(spec=ds.Dataset)
+        dataset.to_table.return_value.to_pandas.return_value = expected_df
+        mock_dataset.return_value = dataset
+
+        # Call the get_changes method
+        changes_df = self.instance.get_changes("table1")
+
+        # Assert that ds.dataset is called with the correct arguments
+        mock_dataset.assert_called_once_with(
+            "test_bucket/path/to/eimer/table1_changes/",
+            format="parquet",
+            partitioning="hive",
+            filesystem=ANY,
+        )
+
+        # Assert that the returned DataFrame is the same as the mock DataFrame
+        self.assertIs(expected_df, changes_df)
+
+    @patch("eimerdb.instance.AuthClient.fetch_google_credentials")
+    @patch("eimerdb.instance.storage.Client")
+    @patch("eimerdb.instance.FileClient.get_gcs_file_system")
+    @patch("eimerdb.instance.uuid4")
+    @patch("eimerdb.instance.pq.write_to_dataset")
+    @patch("eimerdb.instance.EimerDBInstance.get_changes")
+    def test_combine_changes(
+        self,
+        mock_get_changes: Mock,
+        mock_write_to_dataset: Mock,
+        mock_uuid4: Mock,
+        _: Mock,
+        mock_client: Mock,
+        mock_fetch_credentials: Mock,
+    ) -> None:
+        # Mock the return value of get_changes
+        mock_get_changes.return_value = pd.DataFrame([{"row_id": "1", "field1": 1}])
+
+        # Mock the return values of other dependencies
+        mock_uuid4.return_value = "mocked_uuid"
+
+        blob_1 = Mock(spec=Blob)
+        blob_2 = Mock(spec=Blob)
+
+        mock_fetch_credentials.return_value = "token"
+        mock_client.return_value.bucket.return_value.list_blobs.return_value = [
+            blob_1,
+            blob_2,
+        ]
+
+        # Call the merge_changes method
+        self.instance.combine_changes("table1")
+
+        # Assert that the dependencies are called with the expected arguments
+        mock_write_to_dataset.assert_called_once_with(
+            table=pa.Table.from_pandas(mock_get_changes.return_value),
+            root_path="gs://test_bucket/path/to/eimer/table1_changes",
+            partition_cols=None,
+            basename_template="merged_commit_mocked_uuid_{i}.parquet",
+            filesystem=ANY,
+        )
+        blob_1.delete.assert_called_once()
+        blob_2.delete.assert_called_once()
+
+    @patch("eimerdb.instance.FileClient.get_gcs_file_system")
+    @patch("eimerdb.instance.pq.read_table")
+    @patch("eimerdb.instance.duckdb.connect")
+    @patch("eimerdb.instance.pa.concat_tables")
+    @patch("eimerdb.instance.pd.concat")
+    @patch("eimerdb.instance.pd.DataFrame")
+    def test_query_changes(
+        self,
+        mock_pd_dataframe: Mock,
+        mock_pd_concat: Mock,
+        mock_pa_concat_tables: Mock,
+        mock_duckdb_connect: Mock,
+        mock_pq_read_table: Mock,
+        mock_fileclient_gcs: Mock,
+    ) -> None:
+        # Set up mocks
+        mock_pd_dataframe.return_value = Mock()
+        mock_pd_concat.return_value = Mock()
+        mock_pa_concat_tables.return_value = Mock()
+
+        mock_duckdb_connect.return_value.execute.return_value.df.return_value = Mock()
+        mock_duckdb_connect.return_value.execute.return_value.arrow.return_value = (
+            Mock()
+        )
+        mock_pq_read_table.return_value = Mock()
+
+        mock_fileclient_gcs.glob.return_value = [
+            "gs://example_bucket/eimerdb/example_instance/example_table_suffix/date=2022-01-01/file1.parquet",
+            "gs://example_bucket/eimerdb/example_instance/example_table_suffix/date=2022-01-01/file2.parquet",
+        ]
+
+        # Test SELECT
+        result = self.instance.query_changes("SELECT * FROM table1 WHERE condition")
+        self.assertIsInstance(result, Mock)
+
+        # Test UPDATE
+        result = self.instance.query_changes(
+            "UPDATE table1 SET row_id = 1 WHERE row_id = 2"
+        )
+        self.assertIsNone(result)
diff --git a/tests/test_instance_non_admin_user.py b/tests/test_instance_non_admin_user.py
new file mode 100644
index 0000000..f8e88a1
--- /dev/null
+++ b/tests/test_instance_non_admin_user.py
@@ -0,0 +1,66 @@
+import unittest
+from unittest.mock import Mock
+from unittest.mock import patch
+
+import pandas as pd
+
+from eimerdb.instance import EimerDBInstance
+
+
+class TestEimerDBInstanceNonAdminUser(unittest.TestCase):
+    @patch("eimerdb.instance.get_json")
+    @patch("eimerdb.instance.get_initials", return_value="non_admin_user")
+    def setUp(self, _: Mock, mock_get_json: Mock) -> None:
+        mock_get_json.side_effect = [
+            {
+                "eimerdb_name": "test_eimerdb",
+                "path": "/path/to/config",
+                "eimer_path": "/path/to/eimer",
+                "created_by": "admin_user",
+                "time_created": "2024-03-09T12:00:00Z",
+            },
+            {"non_admin_user": "user"},
+            {"admin_user": {"admin_group": ["admin_user"]}},
+            {"table1": {"created_by": "admin_user"}},
+        ]
+
+        self.instance = EimerDBInstance("test_bucket", "test_eimer")
+
+    def test_init_non_admin(self) -> None:
+        self.assertEqual(self.instance.created_by, "admin_user")
+        self.assertEqual(self.instance.users, {"non_admin_user": ""})
+        self.assertIsNone(self.instance.role_groups)
+        self.assertEqual(self.instance.is_admin, False)
+
+    def test_add_user_non_admin(self) -> None:
+        # Test & Assertion
+        with self.assertRaises(PermissionError) as context:
+            self.instance.add_user("new_user", "user")
+        self.assertEqual(
+            "Cannot add user. You are not an admin!", str(context.exception)
+        )
+
+    def test_remove_user_non_admin(self) -> None:
+        # Test & Assertion
+        with self.assertRaises(PermissionError) as context:
+            self.instance.remove_user("non_admin_user")
+        self.assertEqual(
+            "Cannot remove user. You are not an admin!", str(context.exception)
+        )
+
+    def test_create_table_non_admin(self) -> None:
+        # Test & Assertion
+        with self.assertRaises(PermissionError) as context:
+            self.instance.create_table("table2", [{"name": "field1", "type": "int8"}])
+        self.assertEqual(
+            "Cannot create table. You are not an admin!", str(context.exception)
+        )
+
+    def test_main_table_insert_non_admin(self) -> None:
+        # Test & Assertion
+        with self.assertRaises(PermissionError) as context:
+            self.instance.insert("table1", pd.DataFrame())
+        self.assertEqual(
+            "Cannot insert into main table. You are not an admin!",
+            str(context.exception),
+        )
diff --git a/tests/test_query.py b/tests/test_query.py
new file mode 100644
index 0000000..bcd386b
--- /dev/null
+++ b/tests/test_query.py
@@ -0,0 +1,69 @@
+import unittest
+from unittest.mock import MagicMock
+from unittest.mock import Mock
+from unittest.mock import patch
+
+from eimerdb.query import filter_partitions
+from eimerdb.query import get_partitioned_files
+
+
+class TestQuery(unittest.TestCase):
+
+    @patch("eimerdb.query.FileClient.get_gcs_file_system")
+    def test_get_partitioned_files(self, mock_get_gcs_file_system: Mock) -> None:
+        # Mocking FileClient.get_gcs_file_system()
+        mock_fs = MagicMock()
+        mock_get_gcs_file_system.return_value = mock_fs
+
+        # Example input values
+        table_name = "example_table"
+        instance_name = "example_instance"
+        table_config = {"partition_columns": ["date"], "bucket": "example_bucket"}
+        suffix = "_suffix"
+        partition_select = {"date": ["2022-01-01"]}
+        unedited = True
+
+        # Mocking fs.glob() to return some example file paths
+        mock_fs.glob.return_value = [
+            "gs://example_bucket/eimerdb/example_instance/example_table_suffix/date=2022-01-01/file1.parquet",
+            "gs://example_bucket/eimerdb/example_instance/example_table_suffix/date=2022-01-01/file2.parquet",
+        ]
+
+        # Calling the function under test
+        result = get_partitioned_files(
+            table_name,
+            instance_name,
+            table_config,
+            suffix,
+            mock_fs,
+            partition_select,
+            unedited,
+        )
+
+        # Asserting the result
+        expected_result = [
+            "gs://example_bucket/eimerdb/example_instance/example_table_suffix/date=2022-01-01/file1.parquet",
+            "gs://example_bucket/eimerdb/example_instance/example_table_suffix/date=2022-01-01/file2.parquet",
+        ]
+        self.assertEqual(result, expected_result)
+
+        # Asserting that fs.glob() was called with the correct arguments
+        mock_fs.glob.assert_called_once_with(
+            "gs://example_bucket/eimerdb/example_instance/example_table_suffix/**/*"
+        )
+
+    def test_filter_partitions(self) -> None:
+        table_files = [
+            "file1/partition_col1=value1/partition_col2=value2",
+            "file2/partition_col1=value1/partition_col2=value2",
+            "file3/partition_col1=value2/partition_col2=value3",
+        ]
+        partition_select = {"partition_col1": ["value1"], "partition_col2": ["value2"]}
+        result = filter_partitions(table_files, partition_select)
+        self.assertEqual(
+            result,
+            [
+                "file1/partition_col1=value1/partition_col2=value2",
+                "file2/partition_col1=value1/partition_col2=value2",
+            ],
+        )