diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 2e0b7ac..a6e1021 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -20,11 +20,22 @@
// "postCreateCommand": "pip3 install --user -r requirements.txt",
// Configure tool-specific properties.
+ // Py3.6 support (switch extensions to `pre-release` and `install another version`):
+ // Pylance v2022.6.30
+ // Python v2022.8.1
+ // Python Debugger v2023.1.XXX (pre-release version | debugpy v1.5.1)
+ // Black Formatter v2022.2.0
+ // Isort v2022.1.11601002 (pre-release)
"customizations": {
"vscode": {
"extensions": [
"littlefoxteam.vscode-python-test-adapter",
- "jkillian.custom-local-formatters"
+ "jkillian.custom-local-formatters",
+ "ms-python.vscode-pylance",
+ "ms-python.python",
+ "ms-python.debugpy",
+ "ms-python.black-formatter",
+ "ms-python.isort"
]
}
}
diff --git a/.gitignore b/.gitignore
index 9ee7dce..1593f97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,5 @@ main.dSYM/
.idea
SqliteCloud.egg-info
-src/sqlitecloud/libsqcloud.so
+
+playground.ipynb
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d6f5c64..da41096 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,6 +10,11 @@ repos:
- id: detect-private-key
- id: check-merge-conflict
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster
+ - repo: https://github.com/pycqa/isort
+ rev: 5.10.1
+ hooks:
+ - id: isort
+ name: isort
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 22.8.0
hooks:
@@ -19,11 +24,6 @@ repos:
# pre-commit's default_language_version, see
# https://pre-commit.com/#top_level-default_language_version
language_version: python3.6
- - repo: https://github.com/pycqa/isort
- rev: 5.10.1
- hooks:
- - id: isort
- name: isort
- repo: https://github.com/PyCQA/autoflake
rev: v1.4
hooks:
@@ -34,7 +34,7 @@ repos:
- "--expand-star-imports"
- "--remove-duplicate-keys"
- "--remove-unused-variables"
- - "--remove-unused-variables"
+ - "--remove-all-unused-imports"
- repo: https://github.com/pycqa/flake8
rev: 5.0.4
hooks:
diff --git a/README.md b/README.md
index 609995c..e60777c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Python SDK for SQLite Cloud
+# Driver for SQLite Cloud
@@ -11,74 +11,124 @@

-[SQLiteCloud](https://sqlitecloud.io) is a powerful Python package that allows you to interact with the SQLite Cloud backend server seamlessly. It provides methods for various database operations. This package is designed to simplify database operations in Python applications, making it easier than ever to work with SQLite Cloud.
+- [Driver for SQLite Cloud](#driver-for-sqlite-cloud)
+- [Example](#example)
+- [SQLite Cloud loves sqlite3](#sqlite-cloud-loves-sqlite3)
+- [SQLite Cloud for Pandas DataFrame](#sqlite-cloud-for-pandas-dataframe)
-- Site: [https://sqlitecloud.io](https://sqlitecloud.io/developers)
-- Documentation: https://..._coming!_
-- Source: [https://github.com/sqlitecloud/python](https://github.com/sqlitecloud/python)
+---
+
+[SQLiteCloud](https://sqlitecloud.io) is a powerful Python package that allows you to interact with the SQLite Cloud database seamlessly. It provides methods for various database operations. This package is designed to simplify database operations in Python applications, making it easier than ever to work with SQLite Cloud.
+
+
+#### Compatibility with sqlite3 API
-## Installation
+We aim for full compatibility with the Python built-in [sqlite3](https://docs.python.org/3.6/library/sqlite3.html) API (based on Python [PEP 249](https://peps.python.org/pep-0249)), with the primary distinction being that our driver connects to SQLite Cloud databases. This allows you to migrate your local SQLite databases to SQLite Cloud without needing to modify your existing Python code that uses the sqlite3 API.
-You can install SqliteCloud Package using Python Package Index (PYPI):
+- Documentation: Our API closely follows the sqlite3 API. You can refer to the sqlite3 documentation for most functionality. The list of implemented features are documented [here](https://github.com/sqlitecloud/python/issues/8).
+- Source: [https://github.com/sqlitecloud/python](https://github.com/sqlitecloud/python)
+- Site: [https://sqlitecloud.io](https://sqlitecloud.io/developers)
+
+## Example
```bash
$ pip install sqlitecloud
```
-## Usage
-
-
```python
-from sqlitecloud.client import SqliteCloudClient
-from sqlitecloud.types import SqliteCloudAccount
-```
+import sqlitecloud
-### _Init a connection_
+# Open the connection to SQLite Cloud
+conn = sqlitecloud.connect("sqlitecloud://myhost.sqlite.cloud:8860?apikey=myapikey")
-#### Using explicit configuration
+# You can autoselect the database during the connect call
+# by adding the database name as path of the SQLite Cloud
+# connection string, eg:
+# conn = sqlitecloud.connect("sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?apikey=myapikey")
+db_name = "chinook.sqlite"
+conn.execute(f"USE DATABASE {db_name}")
-```python
-account = SqliteCloudAccount(user, password, host, db_name, port)
-client = SqliteCloudClient(cloud_account=account)
-conn = client.open_connection()
-```
+cursor = conn.execute("SELECT * FROM albums WHERE AlbumId = ?", (1, ))
+result = cursor.fetchone()
-#### _Using string configuration_
+print(result)
-```python
-account = SqliteCloudAccount("sqlitecloud://user:pass@host.com:port/dbname?apikey=myapikey")
-client = SqliteCloudClient(cloud_account=account)
-conn = client.open_connection()
+conn.close()
```
-### _Execute a query_
-You can bind values to parametric queries: you can pass parameters as positional values in an array
+## sqlitecloud loves sqlite3
+
+Is your project based on the `sqlite3` library to interact with a SQLite database?
+
+Just install `sqlitecloud` package from `pip` and change the module name! That's it!
+
+Try it yourself:
+
```python
-result = client.exec_query(
- "SELECT * FROM table_name WHERE id = 1"
- conn=conn
+# import sqlitecloud
+import sqlite3
+
+# comment out the following line...
+conn = sqlite3.connect(":memory:")
+
+# ... and uncomment this line and import the sqlitecloud package
+# (add the database name like in this connection string)
+# conn = sqlitecloud.connect("sqlitecloud://myhost.sqlite.cloud:8860/mydatabase.sqlite?apikey=myapikey")
+
+conn.execute("CREATE TABLE IF NOT EXISTS producers (ProducerId INTEGER PRIMARY KEY, name TEXT, year INTEGER)")
+conn.executemany(
+ "INSERT INTO producers (name, year) VALUES (?, ?)",
+ [("Sony Music Entertainment", 2020), ("EMI Music Publishing", 2021)],
)
-```
-### _Iterate result_
-result is an iterable object
-```python
-for row in result:
+cursor = conn.execute("SELECT * FROM cars")
+
+for row in cursor:
print(row)
```
-### _Specific value_
-```python
-result.get_value(0, 0)
-```
+## SQLite Cloud for Pandas DataFrame
-### _Column name_
-```python
-result.get_name(0)
-```
+[Pandas](https://pypi.org/project/pandas/) is a Python package for data manipulation and analysis. It provides high-performance, easy-to-use data structures, such as DataFrame.
+
+Use the connection to SQLite Cloud to:
+- Insert data from a DataFrame into a SQLite Cloud database.
+- Query SQLite Cloud and fetch the results into a DataFrame for further analysis.
-### _Close connection_
+Example:
```python
-client.disconnect(conn)
+import io
+
+import pandas as pd
+
+import sqlitecloud
+
+dfprices = pd.read_csv(
+ io.StringIO(
+ """DATE,CURRENCY,PRICE
+ 20230504,USD,201.23456
+ 20230503,USD,12.34567
+ 20230502,USD,23.45678
+ 20230501,USD,34.56789"""
+ )
+)
+
+conn = sqlitecloud.connect("sqlitecloud://myhost.sqlite.cloud:8860/mydatabase.sqlite?apikey=myapikey")
+
+conn.executemany("DROP TABLE IF EXISTS ?", [("PRICES",)])
+
+# Write the dataframe to the SQLite Cloud database as a table PRICES
+dfprices.to_sql("PRICES", conn, index=False)
+
+# Create the dataframe from the table PRICES on the SQLite Cloud database
+df_actual_prices = pd.read_sql("SELECT * FROM PRICES", conn)
+
+# Inspect the dataframe
+print(df_actual_prices.head())
+
+# Perform a simple query on the dataframe
+query_result = df_actual_prices.query("PRICE > 50.00")
+
+print(query_result)
```
diff --git a/bandit-baseline.json b/bandit-baseline.json
index b53ebef..b3b2040 100644
--- a/bandit-baseline.json
+++ b/bandit-baseline.json
@@ -1,6 +1,6 @@
{
"errors": [],
- "generated_at": "2024-05-23T09:59:11Z",
+ "generated_at": "2024-06-03T07:52:17Z",
"metrics": {
"_totals": {
"CONFIDENCE.HIGH": 0.0,
@@ -11,7 +11,7 @@
"SEVERITY.LOW": 1.0,
"SEVERITY.MEDIUM": 2.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 2096,
+ "loc": 3405,
"nosec": 0
},
"src/setup.py": {
@@ -23,7 +23,7 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 39,
+ "loc": 29,
"nosec": 0
},
"src/sqlitecloud/__init__.py": {
@@ -35,7 +35,7 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 1,
+ "loc": 3,
"nosec": 0
},
"src/sqlitecloud/client.py": {
@@ -47,10 +47,10 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 119,
+ "loc": 104,
"nosec": 0
},
- "src/sqlitecloud/conn_info.py": {
+ "src/sqlitecloud/dbapi2.py": {
"CONFIDENCE.HIGH": 0.0,
"CONFIDENCE.LOW": 0.0,
"CONFIDENCE.MEDIUM": 0.0,
@@ -59,7 +59,7 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 6,
+ "loc": 377,
"nosec": 0
},
"src/sqlitecloud/download.py": {
@@ -83,7 +83,7 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 727,
+ "loc": 765,
"nosec": 0
},
"src/sqlitecloud/pubsub.py": {
@@ -107,7 +107,7 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 62,
+ "loc": 80,
"nosec": 0
},
"src/sqlitecloud/types.py": {
@@ -119,7 +119,7 @@
"SEVERITY.LOW": 1.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 143,
+ "loc": 194,
"nosec": 0
},
"src/sqlitecloud/upload.py": {
@@ -155,7 +155,7 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 22,
+ "loc": 35,
"nosec": 0
},
"src/tests/integration/__init__.py": {
@@ -179,7 +179,19 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 471,
+ "loc": 492,
+ "nosec": 0
+ },
+ "src/tests/integration/test_dbapi2.py": {
+ "CONFIDENCE.HIGH": 0.0,
+ "CONFIDENCE.LOW": 0.0,
+ "CONFIDENCE.MEDIUM": 0.0,
+ "CONFIDENCE.UNDEFINED": 0.0,
+ "SEVERITY.HIGH": 0.0,
+ "SEVERITY.LOW": 0.0,
+ "SEVERITY.MEDIUM": 0.0,
+ "SEVERITY.UNDEFINED": 0.0,
+ "loc": 181,
"nosec": 0
},
"src/tests/integration/test_download.py": {
@@ -206,6 +218,18 @@
"loc": 18,
"nosec": 0
},
+ "src/tests/integration/test_pandas.py": {
+ "CONFIDENCE.HIGH": 0.0,
+ "CONFIDENCE.LOW": 0.0,
+ "CONFIDENCE.MEDIUM": 0.0,
+ "CONFIDENCE.UNDEFINED": 0.0,
+ "SEVERITY.HIGH": 0.0,
+ "SEVERITY.LOW": 0.0,
+ "SEVERITY.MEDIUM": 0.0,
+ "SEVERITY.UNDEFINED": 0.0,
+ "loc": 50,
+ "nosec": 0
+ },
"src/tests/integration/test_pubsub.py": {
"CONFIDENCE.HIGH": 0.0,
"CONFIDENCE.LOW": 1.0,
@@ -215,7 +239,19 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 1.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 109,
+ "loc": 118,
+ "nosec": 0
+ },
+ "src/tests/integration/test_sqlite3_parity.py": {
+ "CONFIDENCE.HIGH": 0.0,
+ "CONFIDENCE.LOW": 0.0,
+ "CONFIDENCE.MEDIUM": 0.0,
+ "CONFIDENCE.UNDEFINED": 0.0,
+ "SEVERITY.HIGH": 0.0,
+ "SEVERITY.LOW": 0.0,
+ "SEVERITY.MEDIUM": 0.0,
+ "SEVERITY.UNDEFINED": 0.0,
+ "loc": 176,
"nosec": 0
},
"src/tests/integration/test_upload.py": {
@@ -242,6 +278,18 @@
"loc": 48,
"nosec": 0
},
+ "src/tests/unit/test_dbapi2.py": {
+ "CONFIDENCE.HIGH": 0.0,
+ "CONFIDENCE.LOW": 0.0,
+ "CONFIDENCE.MEDIUM": 0.0,
+ "CONFIDENCE.UNDEFINED": 0.0,
+ "SEVERITY.HIGH": 0.0,
+ "SEVERITY.LOW": 0.0,
+ "SEVERITY.MEDIUM": 0.0,
+ "SEVERITY.UNDEFINED": 0.0,
+ "loc": 242,
+ "nosec": 0
+ },
"src/tests/unit/test_driver.py": {
"CONFIDENCE.HIGH": 0.0,
"CONFIDENCE.LOW": 0.0,
@@ -251,7 +299,7 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 76,
+ "loc": 188,
"nosec": 0
},
"src/tests/unit/test_resultset.py": {
@@ -263,32 +311,32 @@
"SEVERITY.LOW": 0.0,
"SEVERITY.MEDIUM": 0.0,
"SEVERITY.UNDEFINED": 0.0,
- "loc": 77,
+ "loc": 113,
+ "nosec": 0
+ },
+ "src/tests/unit/test_types.py": {
+ "CONFIDENCE.HIGH": 0.0,
+ "CONFIDENCE.LOW": 0.0,
+ "CONFIDENCE.MEDIUM": 0.0,
+ "CONFIDENCE.UNDEFINED": 0.0,
+ "SEVERITY.HIGH": 0.0,
+ "SEVERITY.LOW": 0.0,
+ "SEVERITY.MEDIUM": 0.0,
+ "SEVERITY.UNDEFINED": 0.0,
+ "loc": 14,
"nosec": 0
}
},
"results": [
{
- "code": "95 class SqliteCloudAccount:\n96 def __init__(\n97 self,\n98 username: Optional[str] = \"\",\n99 password: Optional[str] = \"\",\n100 hostname: Optional[str] = \"\",\n101 dbname: Optional[str] = \"\",\n102 port: Optional[int] = SQCLOUD_DEFAULT.PORT.value,\n103 apikey: Optional[str] = \"\",\n104 ) -> None:\n105 # User name is required unless connectionstring is provided\n106 self.username = username\n107 # Password is required unless connection string is provided\n108 self.password = password\n109 # Password is hashed\n110 self.password_hashed = False\n111 # API key instead of username and password\n112 self.apikey = apikey\n113 # Name of database to open\n114 self.dbname = dbname\n115 # Like mynode.sqlitecloud.io\n116 self.hostname = hostname\n117 self.port = port\n118 \n",
+ "code": "107 class SQLiteCloudAccount:\n108 def __init__(\n109 self,\n110 username: Optional[str] = \"\",\n111 password: Optional[str] = \"\",\n112 hostname: str = \"\",\n113 dbname: Optional[str] = \"\",\n114 port: int = SQLITECLOUD_DEFAULT.PORT.value,\n115 apikey: Optional[str] = \"\",\n116 ) -> None:\n117 # User name is required unless connectionstring is provided\n118 self.username = username\n119 # Password is required unless connection string is provided\n120 self.password = password\n121 # Password is hashed\n122 self.password_hashed = False\n123 # API key instead of username and password\n124 self.apikey = apikey\n125 # Name of database to open\n126 self.dbname = dbname\n127 # Like mynode.sqlitecloud.io\n128 self.hostname = hostname\n129 self.port = port\n130 \n",
"col_offset": 4,
"filename": "src/sqlitecloud/types.py",
"issue_confidence": "MEDIUM",
"issue_severity": "LOW",
"issue_text": "Possible hardcoded password: ''",
- "line_number": 96,
+ "line_number": 108,
"line_range": [
- 96,
- 97,
- 98,
- 99,
- 100,
- 101,
- 102,
- 103,
- 104,
- 105,
- 106,
- 107,
108,
109,
110,
@@ -298,22 +346,34 @@
114,
115,
116,
- 117
+ 117,
+ 118,
+ 119,
+ 120,
+ 121,
+ 122,
+ 123,
+ 124,
+ 125,
+ 126,
+ 127,
+ 128,
+ 129
],
"more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html",
"test_id": "B107",
"test_name": "hardcoded_password_default"
},
{
- "code": "155 client.exec_query(\n156 f\"UPDATE genres SET Name = '{new_name}' WHERE GenreId = 1;\", connection\n157 )\n",
+ "code": "164 client.exec_query(\n165 f\"UPDATE genres SET Name = '{new_name}' WHERE GenreId = 1;\", connection\n166 )\n",
"col_offset": 12,
"filename": "src/tests/integration/test_pubsub.py",
"issue_confidence": "LOW",
"issue_severity": "MEDIUM",
"issue_text": "Possible SQL injection vector through string-based query construction.",
- "line_number": 156,
+ "line_number": 165,
"line_range": [
- 156
+ 165
],
"more_info": "https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html",
"test_id": "B608",
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 1f30ce7..207f9a6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -14,3 +14,7 @@ isort==5.10.1
autoflake==1.4
pre-commit==2.17.0
bandit==1.7.1
+# We can use the most recent compatible version because
+# this package is only used for testing compatibility
+# with pandas dataframe
+pandas>=1.1.5
diff --git a/samples.ipynb b/samples.ipynb
index 7f9d9e7..7a35b90 100644
--- a/samples.ipynb
+++ b/samples.ipynb
@@ -4,130 +4,263 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Import SqliteCloudClient and SqliteCloudAccount\n",
+ "# How to query SQLite Cloud database (SQLite3 API)\n",
"\n",
- "SqliteCloudAccount is the class rapresenting your auth data for SqliteCloud\n",
+ "Before start:\n",
"\n",
- "SqliteCloudClient is the class managing the connection for you"
+ "1. Create an account for free on [sqlitecloud.io](https://sqlitecloud.io/).\n",
+ "2. Copy your connection string (keep it secret! It contains your API KEY).\n",
+ "\n",
+ "Let's start!"
]
},
{
"cell_type": "code",
- "execution_count": 1,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "import sys\n",
- "\n",
- "sys.path.append('/workspaces/python/src')\n",
- "\n",
- "from sqlitecloud.client import SqliteCloudClient\n",
- "from sqlitecloud.types import SqliteCloudAccount"
+ "%pip install sqlitecloud"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": 3,
"metadata": {},
+ "outputs": [],
"source": [
- "## Init a connection\n",
+ "import sqlitecloud\n",
"\n",
- "Initialize the client with account connection info"
+ "# You can autoselect the database during the connection by adding the \n",
+ "# database name as path of the SQLite Cloud connection string, eg:\n",
+ "# conn = sqlitecloud.connect(\"sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?apikey=myapikey\")\n",
+ "sqlitecloud_connection_string = \"sqlitecloud://myhost.sqlite.cloud:8860/mydatabase?apikey=myapikey\"\n"
]
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
- "account = SqliteCloudAccount(user, password, host, db_name, 8860)\n",
- "client = SqliteCloudClient(cloud_account=account)\n",
- "conn = client.open_connection()"
+ "# Open the connection to SQLite Cloud\n",
+ "conn = sqlitecloud.connect(sqlitecloud_connection_string)\n"
]
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "'chinook.sqlite'"
+ ""
]
},
- "execution_count": 10,
+ "execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "db_name"
+ "# Select the database to use if not specified in the connection string\n",
+ "db_name = \"chinook.sqlite\"\n",
+ "conn.execute(f\"USE DATABASE {db_name}\")\n"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": 6,
"metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(1, 'For Those About To Rock We Salute You', 1)"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
- "Then execute the query"
+ "# The execution of the query generate the `Cursor` object\n",
+ "# to fetch the results.\n",
+ "cursor = conn.execute(\"SELECT * FROM albums\")\n",
+ "cursor.fetchone()\n"
]
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(31, 'My brand new genre')"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# You can use the cursor to perform other queries.\n",
+ "# Queries can be prepared with `question mark` and `named` style\n",
+ "cursor = conn.execute(\"INSERT INTO genres (Name) values (?)\", (\"My brand new genre\",))\n",
+ "\n",
+ "cursor.execute(\"SELECT * FROM genres WHERE Name like :name\", {\"name\": \"My brand%\"})\n",
+ "\n",
+ "cursor.fetchone()\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
- "query = \"select * from employees;\"\n",
- "result = client.exec_query(query, conn)"
+ "# When you are done clean up the connection\n",
+ "conn.close()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "The result is an iterable"
+ "### sqlitecloud loves sqlite3\n",
+ "\n",
+ "Is your project based on the `sqlite3` library to interact with a SQLite database?\n",
+ "\n",
+ "Just install `sqlitecloud` package from `pip` and change the module name! That's it!\n",
+ "\n",
+ "Try it yourself:"
]
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "{'EmployeeId': '1', 'LastName': 'Adams', 'FirstName': 'Andrew', 'Title': 'General Manager', 'ReportsTo': None, 'BirthDate': '1962-02-18 00:00:00', 'HireDate': '2002-08-14 00:00:00', 'Address': '11120 Jasper Ave NW', 'City': 'Edmonton', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T5K 2N1', 'Phone': '+1 (780) 428-9482', 'Fax': '+1 (780) 428-3457', 'Email': 'andrew@chinookcorp.com'}\n",
- "{'EmployeeId': '2', 'LastName': 'Edwards', 'FirstName': 'Nancy', 'Title': 'Sales Manager', 'ReportsTo': '1', 'BirthDate': '1958-12-08 00:00:00', 'HireDate': '2002-05-01 00:00:00', 'Address': '825 8 Ave SW', 'City': 'Calgary', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T2P 2T3', 'Phone': '+1 (403) 262-3443', 'Fax': '+1 (403) 262-3322', 'Email': 'nancy@chinookcorp.com'}\n",
- "{'EmployeeId': '3', 'LastName': 'Peacock', 'FirstName': 'Jane', 'Title': 'Sales Support Agent', 'ReportsTo': '2', 'BirthDate': '1973-08-29 00:00:00', 'HireDate': '2002-04-01 00:00:00', 'Address': '1111 6 Ave SW', 'City': 'Calgary', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T2P 5M5', 'Phone': '+1 (403) 262-3443', 'Fax': '+1 (403) 262-6712', 'Email': 'jane@chinookcorp.com'}\n",
- "{'EmployeeId': '4', 'LastName': 'Park', 'FirstName': 'Margaret', 'Title': 'Sales Support Agent', 'ReportsTo': '2', 'BirthDate': '1947-09-19 00:00:00', 'HireDate': '2003-05-03 00:00:00', 'Address': '683 10 Street SW', 'City': 'Calgary', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T2P 5G3', 'Phone': '+1 (403) 263-4423', 'Fax': '+1 (403) 263-4289', 'Email': 'margaret@chinookcorp.com'}\n",
- "{'EmployeeId': '5', 'LastName': 'Johnson', 'FirstName': 'Steve', 'Title': 'Sales Support Agent', 'ReportsTo': '2', 'BirthDate': '1965-03-03 00:00:00', 'HireDate': '2003-10-17 00:00:00', 'Address': '7727B 41 Ave', 'City': 'Calgary', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T3B 1Y7', 'Phone': '1 (780) 836-9987', 'Fax': '1 (780) 836-9543', 'Email': 'steve@chinookcorp.com'}\n",
- "{'EmployeeId': '6', 'LastName': 'Mitchell', 'FirstName': 'Michael', 'Title': 'IT Manager', 'ReportsTo': '1', 'BirthDate': '1973-07-01 00:00:00', 'HireDate': '2003-10-17 00:00:00', 'Address': '5827 Bowness Road NW', 'City': 'Calgary', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T3B 0C5', 'Phone': '+1 (403) 246-9887', 'Fax': '+1 (403) 246-9899', 'Email': 'michael@chinookcorp.com'}\n",
- "{'EmployeeId': '7', 'LastName': 'King', 'FirstName': 'Robert', 'Title': 'IT Staff', 'ReportsTo': '6', 'BirthDate': '1970-05-29 00:00:00', 'HireDate': '2004-01-02 00:00:00', 'Address': '590 Columbia Boulevard West', 'City': 'Lethbridge', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T1K 5N8', 'Phone': '+1 (403) 456-9986', 'Fax': '+1 (403) 456-8485', 'Email': 'robert@chinookcorp.com'}\n",
- "{'EmployeeId': '8', 'LastName': 'Callahan', 'FirstName': 'Laura', 'Title': 'IT Staff', 'ReportsTo': '6', 'BirthDate': '1968-01-09 00:00:00', 'HireDate': '2004-03-04 00:00:00', 'Address': '923 7 ST NW', 'City': 'Lethbridge', 'State': 'AB', 'Country': 'Canada', 'PostalCode': 'T1H 1Y8', 'Phone': '+1 (403) 467-3351', 'Fax': '+1 (403) 467-8772', 'Email': 'laura@chinookcorp.com'}\n"
+ "(1, 'Sony Music Entertainment', 2020)\n",
+ "(2, 'EMI Music Publishing', 2021)\n"
]
}
],
"source": [
- "for r in result:\n",
- " print(r)"
+ "import sqlite3\n",
+ "\n",
+ "import sqlitecloud\n",
+ "\n",
+ "# Comment out the following line\n",
+ "conn = sqlite3.connect(\":memory:\")\n",
+ "# and uncomment this one to use the sqlitecloud package\n",
+ "# conn = sqlitecloud.connect(sqlitecloud_connection_string)\n",
+ "\n",
+ "conn.execute(\n",
+ " \"CREATE TABLE IF NOT EXISTS producers (ProducerId INTEGER PRIMARY KEY, name TEXT, year INTEGER)\"\n",
+ ")\n",
+ "conn.executemany(\n",
+ " \"INSERT INTO producers (name, year) VALUES (?, ?)\",\n",
+ " [(\"Sony Music Entertainment\", 2020), (\"EMI Music Publishing\", 2021)],\n",
+ ")\n",
+ "\n",
+ "cursor = conn.execute(\"SELECT * FROM producers\")\n",
+ "\n",
+ "for row in cursor:\n",
+ " print(row)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Whe you are done clean up the connection"
+ "### SQLite Cloud for Pandas DataFrame\n",
+ "\n",
+ "[Pandas](https://pypi.org/project/pandas/) is a Python package for data manipulation and analysis. It provides high-performance, easy-to-use data structures, such as DataFrame.\n",
+ "\n",
+ "Use the connection to SQLite Cloud to:\n",
+ "- Insert data from a DataFrame into a SQLite Cloud database.\n",
+ "- Query SQLite Cloud and fetch the results into a DataFrame for further analysis.\n",
+ "\n",
+ "Example:"
]
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 10,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " DATE CURRENCY PRICE\n",
+ "0 20230504 USD 201.23456\n",
+ "1 20230503 USD 12.34567\n",
+ "2 20230502 USD 23.45678\n",
+ "3 20230501 USD 34.56789\n"
+ ]
+ }
+ ],
"source": [
- "client.disconnect(conn)\n"
+ "import io\n",
+ "\n",
+ "import pandas as pd\n",
+ "\n",
+ "import sqlitecloud\n",
+ "\n",
+ "dfprices = pd.read_csv(\n",
+ " io.StringIO(\n",
+ " \"\"\"DATE,CURRENCY,PRICE\n",
+ " 20230504,USD,201.23456\n",
+ " 20230503,USD,12.34567\n",
+ " 20230502,USD,23.45678\n",
+ " 20230501,USD,34.56789\"\"\"\n",
+ " )\n",
+ ")\n",
+ "\n",
+ "conn = sqlitecloud.connect(sqlitecloud_connection_string)\n",
+ "\n",
+ "conn.executemany(\"DROP TABLE IF EXISTS ?\", [(\"PRICES\",)])\n",
+ "\n",
+ "# Write the dataframe to the SQLite Cloud database as a table PRICES\n",
+ "dfprices.to_sql(\"PRICES\", conn, index=False)\n",
+ "\n",
+ "# Create the dataframe from the table PRICES on the SQLite Cloud database\n",
+ "df_actual_prices = pd.read_sql(\"SELECT * FROM PRICES\", conn)\n",
+ "\n",
+ "# Inspect the dataframe\n",
+ "print(df_actual_prices.head())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " DATE CURRENCY PRICE\n",
+ "0 20230504 USD 201.23456\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Perform a simple query on the dataframe\n",
+ "query_result = df_actual_prices.query(\"PRICE > 50.00\")\n",
+ "\n",
+ "print(query_result)"
]
}
],
diff --git a/src/setup.py b/src/setup.py
index 1760435..e653ac5 100644
--- a/src/setup.py
+++ b/src/setup.py
@@ -7,7 +7,7 @@
long_description = (Path(__file__).parent / "README.md").read_text()
setup(
- name="SqliteCloud",
+ name="sqlitecloud",
version="0.0.77",
author="sqlitecloud.io",
description="A Python package for working with SQLite databases in the cloud.",
diff --git a/src/sqlitecloud/__init__.py b/src/sqlitecloud/__init__.py
index 640940d..7318b7b 100644
--- a/src/sqlitecloud/__init__.py
+++ b/src/sqlitecloud/__init__.py
@@ -1 +1,9 @@
+# To replicate the public interface of sqlite3, we need to import
+# the classes and functions from the dbapi2 module.
+# eg: sqlite3.connect() -> sqlitecloud.connect()
+#
+from .dbapi2 import Connection, Cursor, connect
+
+__all__ = ["VERSION", "Connection", "Cursor", "connect"]
+
VERSION = "0.0.77"
diff --git a/src/sqlitecloud/client.py b/src/sqlitecloud/client.py
index cff4824..375fd36 100644
--- a/src/sqlitecloud/client.py
+++ b/src/sqlitecloud/client.py
@@ -1,28 +1,27 @@
""" Module to interact with remote SqliteCloud database
"""
-from typing import Optional
-from urllib import parse
+from typing import Dict, Optional, Tuple, Union
from sqlitecloud.driver import Driver
-from sqlitecloud.resultset import SqliteCloudResultSet
+from sqlitecloud.resultset import SQLiteCloudResultSet
from sqlitecloud.types import (
- SQCLOUD_DEFAULT,
- SQCloudConfig,
- SQCloudConnect,
- SQCloudException,
- SqliteCloudAccount,
+ SQLiteCloudAccount,
+ SQLiteCloudConfig,
+ SQLiteCloudConnect,
+ SQLiteCloudDataTypes,
+ SQLiteCloudException,
)
-class SqliteCloudClient:
+class SQLiteCloudClient:
"""
Client to interact with Sqlite Cloud
"""
def __init__(
self,
- cloud_account: Optional[SqliteCloudAccount] = None,
+ cloud_account: Optional[SQLiteCloudAccount] = None,
connection_str: Optional[str] = None,
) -> None:
"""Initializes a new instance of the class with connection information.
@@ -36,24 +35,25 @@ def __init__(
"""
self._driver = Driver()
- self.config = SQCloudConfig()
+ self.config = SQLiteCloudConfig()
if connection_str:
- self.config = self._parse_connection_string(connection_str)
+ self.config = SQLiteCloudConfig(connection_str)
elif cloud_account:
self.config.account = cloud_account
- else:
- raise SQCloudException("Missing connection parameters")
- def open_connection(self) -> SQCloudConnect:
- """Opens a connection to the SQCloud server.
+ if self.config.account is None:
+ raise SQLiteCloudException("Missing connection parameters")
+
+ def open_connection(self) -> SQLiteCloudConnect:
+ """Opens a connection to the SQLite Cloud server.
Returns:
- SQCloudConnect: An instance of the SQCloudConnect class representing
- the connection to the SQCloud server.
+ SQLiteCloudConnect: An instance of the SQLiteCloudConnect class representing
+ the connection to the SQLite Cloud server.
Raises:
- SQCloudException: If an error occurs while opening the connection.
+ SQLiteCloudException: If an error occurs while opening the connection.
"""
connection = self._driver.connect(
self.config.account.hostname, self.config.account.port, self.config
@@ -61,97 +61,77 @@ def open_connection(self) -> SQCloudConnect:
return connection
- def disconnect(self, conn: SQCloudConnect) -> None:
+ def disconnect(self, conn: SQLiteCloudConnect) -> None:
"""Close the connection to the database."""
self._driver.disconnect(conn)
- def is_connected(self, conn: SQCloudConnect) -> bool:
+ def is_connected(self, conn: SQLiteCloudConnect) -> bool:
"""Check if the connection is still open.
Args:
- conn (SQCloudConnect): The connection to the database.
+ conn (SQLiteCloudConnect): The connection to the database.
Returns:
bool: True if the connection is open, False otherwise.
"""
return self._driver.is_connected(conn)
- def exec_query(self, query: str, conn: SQCloudConnect) -> SqliteCloudResultSet:
+ def exec_query(self, query: str, conn: SQLiteCloudConnect) -> SQLiteCloudResultSet:
"""Executes a SQL query on the SQLite Cloud database.
Args:
- query (str): The SQL query to be executed.
+ query (str): The SQL query to execute.
Returns:
SqliteCloudResultSet: The result set of the executed query.
Raises:
- SQCloudException: If an error occurs while executing the query.
+ SQLiteCloudException: If an error occurs while executing the query.
"""
result = self._driver.execute(query, conn)
- return SqliteCloudResultSet(result)
+ return SQLiteCloudResultSet(result)
+
+ def exec_statement(
+ self,
+ query: str,
+ parameters: Union[
+ Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]
+ ],
+ conn: SQLiteCloudConnect,
+ ) -> SQLiteCloudResultSet:
+ """
+ Prepare and execute a SQL statement (either a query or command) to the SQLite Cloud database.
+ This function supports two styles of parameter markers:
+
+ 1. Question Mark Style: Parameters are passed as a tuple. For example:
+ "SELECT * FROM table WHERE id = ?"
+
+ 2. Named Style: Parameters are passed as a dictionary. For example:
+ "SELECT * FROM table WHERE id = :id"
- def sendblob(self, blob: bytes, conn: SQCloudConnect) -> SqliteCloudResultSet:
+ In both cases, the parameters replace the placeholders in the SQL statement.
+
+ Args:
+ query (str): The SQL query to execute.
+ parameters (Union[Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]]):
+ The parameters to be used in the query. It can be a tuple or a dictionary.
+ conn (SQLiteCloudConnect): The connection object to use for executing the query.
+
+ Returns:
+ SqliteCloudResultSet: The result set obtained from executing the query.
+ """
+ prepared_statement = self._driver.prepare_statement(query, parameters)
+
+ result = self._driver.execute(prepared_statement, conn)
+
+ return SQLiteCloudResultSet(result)
+
+ def sendblob(self, blob: bytes, conn: SQLiteCloudConnect) -> SQLiteCloudResultSet:
"""Sends a blob to the SQLite database.
Args:
blob (bytes): The blob to be sent to the database.
- conn (SQCloudConnect): The connection to the database.
+ conn (SQLiteCloudConnect): The connection to the database.
"""
return self._driver.send_blob(blob, conn)
-
- def _parse_connection_string(self, connection_string) -> SQCloudConfig:
- # URL STRING FORMAT
- # sqlitecloud://user:pass@host.com:port/dbname?timeout=10&key2=value2&key3=value3
- # or sqlitecloud://host.sqlite.cloud:8860/dbname?apikey=zIiAARzKm9XBVllbAzkB1wqrgijJ3Gx0X5z1A4m4xBA
-
- config = SQCloudConfig()
- config.account = SqliteCloudAccount()
-
- try:
- params = parse.urlparse(connection_string)
-
- options = {}
- query = params.query
- options = parse.parse_qs(query)
- for option, values in options.items():
- opt = option.lower()
- value = values.pop()
-
- if value.lower() in ["true", "false"]:
- value = bool(value)
- elif value.isdigit():
- value = int(value)
- else:
- value = value
-
- if hasattr(config, opt):
- setattr(config, opt, value)
- elif hasattr(config.account, opt):
- setattr(config.account, opt, value)
-
- # apikey or username/password is accepted
- if not config.account.apikey:
- config.account.username = (
- parse.unquote(params.username) if params.username else ""
- )
- config.account.password = (
- parse.unquote(params.password) if params.password else ""
- )
-
- path = params.path
- database = path.strip("/")
- if database:
- config.account.dbname = database
-
- config.account.hostname = params.hostname
- config.account.port = (
- int(params.port) if params.port else SQCLOUD_DEFAULT.PORT.value
- )
-
- return config
- except Exception as e:
- raise SQCloudException(
- f"Invalid connection string {connection_string}"
- ) from e
diff --git a/src/sqlitecloud/dbapi2.py b/src/sqlitecloud/dbapi2.py
new file mode 100644
index 0000000..06ba402
--- /dev/null
+++ b/src/sqlitecloud/dbapi2.py
@@ -0,0 +1,489 @@
+# DB-API 2.0 interface to SQLite Cloud.
+#
+# PEP 249 – Python Database API Specification v2.0
+# https://peps.python.org/pep-0249/
+#
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Tuple,
+ Union,
+ overload,
+)
+
+from sqlitecloud.driver import Driver
+from sqlitecloud.resultset import SQLiteCloudResult
+from sqlitecloud.types import (
+ SQLITECLOUD_RESULT_TYPE,
+ SQLiteCloudAccount,
+ SQLiteCloudConfig,
+ SQLiteCloudConnect,
+ SQLiteCloudDataTypes,
+ SQLiteCloudException,
+)
+
+# Question mark style, e.g. ...WHERE name=?
+# Module also supports Named style, e.g. ...WHERE name=:name
+paramstyle = "qmark"
+
+# Threads may share the module, but not connections
+threadsafety = 1
+
+# DB API level
+apilevel = "2.0"
+
+
+@overload
+def connect(connection_str: str) -> "Connection":
+ """
+ Establishes a connection to the SQLite Cloud database.
+
+ Args:
+ connection_str (str): The connection string for the database.
+ It may include SQLiteCloudConfig'options like timeout, apikey, etc. in the url query string.
+ Eg: sqlitecloud://myhost.sqlitecloud.io:8860/mydb?apikey=abc123&compression=true&timeout=10
+
+ Returns:
+ Connection: A connection object representing the database connection.
+
+ Raises:
+ SQLiteCloudException: If an error occurs while establishing the connection.
+ """
+ ...
+
+
+@overload
+def connect(
+ cloud_account: SQLiteCloudAccount, config: Optional[SQLiteCloudConfig] = None
+) -> "Connection":
+ """
+ Establishes a connection to the SQLite Cloud database using the provided cloud account and configuration.
+
+ Args:
+ cloud_account (SqliteCloudAccount): The cloud account used to authenticate and access the database.
+ config (Optional[SQLiteCloudConfig]): Additional configuration options for the connection (default: None).
+
+ Returns:
+ Connection: A connection object representing the connection to the SQLite Cloud database.
+
+ Raises:
+ SQLiteCloudException: If an error occurs while establishing the connection.
+ """
+ ...
+
+
+def connect(
+ connection_info: Union[str, SQLiteCloudAccount],
+ config: Optional[SQLiteCloudConfig] = None,
+) -> "Connection":
+ """
+ Establishes a connection to the SQLite Cloud database.
+
+ Args:
+ connection_info (Union[str, SqliteCloudAccount]): The connection information.
+ It can be either a connection string or a `SqliteCloudAccount` object.
+ config (Optional[SQLiteCloudConfig]): The configuration options for the connection.
+ Defaults to None.
+
+ Returns:
+ Connection: A DB-API 2.0 connection object representing the connection to the database.
+
+ Raises:
+ SQLiteCloudException: If an error occurs while establishing the connection.
+ """
+ driver = Driver()
+
+ if isinstance(connection_info, SQLiteCloudAccount):
+ if not config:
+ config = SQLiteCloudConfig()
+ config.account = connection_info
+ else:
+ config = SQLiteCloudConfig(connection_info)
+
+ return Connection(
+ driver.connect(config.account.hostname, config.account.port, config)
+ )
+
+
+class Connection:
+ """
+ Represents a DB-APi 2.0 connection to the SQLite Cloud database.
+
+ Args:
+ SQLiteCloud_connection (SQLiteCloudConnect): The SQLite Cloud connection object.
+
+ Attributes:
+ _driver (Driver): The driver object used for database operations.
+ SQLiteCloud_connection (SQLiteCloudConnect): The SQLite Cloud connection object.
+ """
+
+ row_factory: Optional[Callable[["Cursor", Tuple], object]] = None
+
+ def __init__(self, SQLiteCloud_connection: SQLiteCloudConnect) -> None:
+ self._driver = Driver()
+ self.row_factory = None
+ self.SQLiteCloud_connection = SQLiteCloud_connection
+
+ @property
+ def sqlcloud_connection(self) -> SQLiteCloudConnect:
+ """
+ Returns the SQLite Cloud connection object.
+
+ Returns:
+ SQLiteCloudConnect: The SQLite Cloud connection object.
+ """
+ return self.SQLiteCloud_connection
+
+ def execute(
+ self,
+ sql: str,
+ parameters: Union[
+ Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]
+ ] = (),
+ ) -> "Cursor":
+ """
+ Shortcut for cursor.execute().
+ See the docstring of Cursor.execute() for more information.
+
+ Args:
+ sql (str): The SQL query to execute.
+ parameters (Union[Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]]):
+ The parameters to be used in the query. It can be a tuple or a dictionary. (Default ())
+ conn (SQLiteCloudConnect): The connection object to use for executing the query.
+
+ Returns:
+ Cursor: The cursor object.
+ """
+ cursor = self.cursor()
+ return cursor.execute(sql, parameters)
+
+ def executemany(
+ self,
+ sql: str,
+ seq_of_parameters: Iterable[
+ Union[
+ Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]
+ ]
+ ],
+ ) -> "Cursor":
+ """
+ Shortcut for cursor.executemany().
+ See the docstring of Cursor.executemany() for more information.
+
+ Args:
+ sql (str): The SQL statement to execute.
+ seq_of_parameters (Iterable[Union[Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]]]):
+ The sequence of parameter sets to bind to the SQL statement.
+
+ Returns:
+ Cursor: The cursor object.
+ """
+ cursor = self.cursor()
+ return cursor.executemany(sql, seq_of_parameters)
+
+ def close(self):
+ """
+ Closes the database connection.
+ All cursors created with this connection will become unusable after calling this method.
+
+ Note:
+ DB-API 2.0 interface does not manage the Sqlite Cloud PubSub feature.
+ Therefore, only the main socket is closed.
+ """
+ self._driver.disconnect(self.SQLiteCloud_connection, True)
+
+ def commit(self):
+ """
+ Not implementied yet.
+ """
+
+ def rollback(self):
+ """
+ Not implemented yet.
+ """
+
+ def cursor(self):
+ """
+ Creates a new cursor object.
+
+ Returns:
+ Cursor: The cursor object.
+ """
+ cursor = Cursor(self)
+ cursor.row_factory = self.row_factory
+ return cursor
+
+ def __del__(self) -> None:
+ self.close()
+
+
+class Cursor(Iterator[Any]):
+ """
+ The DB-API 2.0 Cursor class represents a database cursor, which is used to interact with the database.
+ It provides methods to execute SQL statements, fetch results, and manage the cursor state.
+
+ Attributes:
+ arraysize (int): The number of rows to fetch at a time with fetchmany(). Default is 1.
+ """
+
+ arraysize: int = 1
+
+ row_factory: Optional[Callable[["Cursor", Tuple], object]] = None
+
+ def __init__(self, connection: Connection) -> None:
+ self._driver = Driver()
+ self.row_factory = None
+ self._connection = connection
+ self._iter_row: int = 0
+ self._resultset: SQLiteCloudResult = None
+
+ @property
+ def connection(self) -> Connection:
+ """
+ Returns the connection object associated with the database.
+
+ Returns:
+ Connection: The DB-API 2.0 connection object.
+ """
+ return self._connection
+
+ @property
+ def description(
+ self,
+ ) -> Optional[Tuple[Tuple[str, None, None, None, None, None, None], ...]]:
+ """
+ Each sequence contains information describing one result column.
+ Only the first value of the tuple is set which represents the column name.
+ """
+ if not self._is_result_rowset():
+ return None
+
+ description = ()
+ for i in range(self._resultset.ncols):
+ description += (
+ (
+ self._resultset.colname[i],
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ ),
+ )
+
+ return description
+
+ @property
+ def rowcount(self) -> int:
+ """
+ The number of rows that the last .execute*() produced (for DQL statements like SELECT)
+
+ The number of rows affected by DML statements like UPDATE or INSERT is not supported.
+
+ Returns:
+ int: The number of rows in the result set or -1 if no result set is available.
+ """
+ return self._resultset.nrows if self._is_result_rowset() else -1
+
+ @property
+ def lastrowid(self) -> Optional[int]:
+ """
+ Not implemented yet in the library.
+ """
+ return None
+
+ def close(self) -> None:
+ """
+ Just mark the cursors to be no more usable in SQLite Cloud database.
+ In sqlite the `close()` is used to free up resources: https://devpress.csdn.net/python/62fe355b7e668234661931d8.html
+ """
+ self._ensure_connection()
+
+ self._connection = None
+
+ def execute(
+ self,
+ sql: str,
+ parameters: Union[
+ Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]
+ ] = (),
+ ) -> "Cursor":
+ """
+ Prepare and execute a SQL statement (either a query or command) to the SQLite Cloud database.
+ This function supports two styles of parameter markers:
+
+ 1. Question Mark Style: Parameters are passed as a tuple. For example:
+ "SELECT * FROM table WHERE id = ?"
+
+ 2. Named Style: Parameters are passed as a dictionary. For example:
+ "SELECT * FROM table WHERE id = :id"
+
+ In both cases, the parameters replace the placeholders in the SQL statement.
+
+ Shortcut for cursor.execute().
+
+ Args:
+ sql (str): The SQL query to execute.
+ parameters (Union[Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]]):
+ The parameters to be used in the query. It can be a tuple or a dictionary. (Default ())
+ conn (SQLiteCloudConnect): The connection object to use for executing the query.
+
+ Returns:
+ Cursor: The cursor object.
+ """
+ self._ensure_connection()
+
+ prepared_statement = self._driver.prepare_statement(sql, parameters)
+ result = self._driver.execute(
+ prepared_statement, self.connection.sqlcloud_connection
+ )
+
+ self._resultset = result
+
+ return self
+
+ def executemany(
+ self,
+ sql: str,
+ seq_of_parameters: Iterable[
+ Union[
+ Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]
+ ]
+ ],
+ ) -> "Cursor":
+ """
+ Executes a SQL statement multiple times, each with a different set of parameters.
+ The entire statement is transmitted to the SQLite Cloud server in a single operation.
+ This method is useful for executing the same query repeatedly with different values.
+
+ Args:
+ sql (str): The SQL statement to execute.
+ seq_of_parameters (Iterable[Union[Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]]]):
+ The sequence of parameter sets to bind to the SQL statement.
+
+ Returns:
+ Cursor: The cursor object.
+ """
+ self._ensure_connection()
+
+ commands = ""
+ for parameters in seq_of_parameters:
+ prepared_statement = self._driver.prepare_statement(sql, parameters)
+ commands += prepared_statement + ";"
+
+ self.execute(commands)
+
+ return self
+
+ def fetchone(self) -> Optional[Any]:
+ """
+ Fetches the next row of a result set, returning it as a single sequence,
+ or None if no more rows are available.
+
+ Returns:
+ The next row of the query result set as a tuple,
+ or None if no more rows are available.
+ """
+ self._ensure_connection()
+
+ if not self._is_result_rowset():
+ return None
+
+ return next(self, None)
+
+ def fetchmany(self, size=None) -> List[Any]:
+ """
+ Fetches the next set of rows from the result set.
+
+ Args:
+ size (int, optional): The maximum number of rows to fetch.
+ If not specified, it uses the `arraysize` attribute.
+
+ Returns:
+ List[Tuple]: A list of rows, where each row is represented as a tuple.
+ """
+ self._ensure_connection()
+
+ if not self._is_result_rowset():
+ return []
+
+ if size is None:
+ size = self.arraysize
+
+ results = []
+ for _ in range(size):
+ next_result = self.fetchone()
+ if next_result is None:
+ break
+ results.append(next_result)
+
+ return results
+
+ def fetchall(self) -> List[Any]:
+ """
+ Fetches all remaining rows of a query result set.
+
+ Returns:
+ A list of rows, where each row is represented as a tuple.
+ """
+ self._ensure_connection()
+
+ if not self._is_result_rowset():
+ return []
+
+ return self.fetchmany(self.rowcount)
+
+ def setinputsizes(self, sizes) -> None:
+ pass
+
+ def setoutputsize(self, size, column=None) -> None:
+ pass
+
+ def _call_row_factory(self, row: Tuple) -> object:
+ if self.row_factory is None:
+ return row
+
+ return self.row_factory(self, row)
+
+ def _is_result_rowset(self) -> bool:
+ return (
+ self._resultset
+ and self._resultset.tag == SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET
+ )
+
+ def _ensure_connection(self):
+ """
+ Ensure the cursor is usable or has been closed.
+
+ Raises:
+ SQLiteCloudException: If the cursor is closed.
+ """
+ if not self._connection:
+ raise SQLiteCloudException("The cursor is closed.")
+
+ def __iter__(self) -> "Cursor":
+ return self
+
+ def __next__(self) -> Optional[Tuple[Any]]:
+ self._ensure_connection()
+
+ if (
+ not self._resultset.is_result
+ and self._resultset.data
+ and self._iter_row < self._resultset.nrows
+ ):
+ out: tuple[Any] = ()
+
+ for col in range(self._resultset.ncols):
+ out += (self._resultset.get_value(self._iter_row, col),)
+ self._iter_row += 1
+
+ return self._call_row_factory(out)
+
+ raise StopIteration
diff --git a/src/sqlitecloud/download.py b/src/sqlitecloud/download.py
index 21ac3dd..37eb8b6 100644
--- a/src/sqlitecloud/download.py
+++ b/src/sqlitecloud/download.py
@@ -2,7 +2,7 @@
from io import BufferedWriter
from sqlitecloud.driver import Driver
-from sqlitecloud.types import SQCloudConnect
+from sqlitecloud.types import SQLiteCloudConnect
def xCallback(
@@ -28,12 +28,12 @@ def xCallback(
logging.log(logging.DEBUG, f"{(nprogress + blen) / ntot * 100:.2f}%")
-def download_db(connection: SQCloudConnect, dbname: str, filename: str) -> None:
+def download_db(connection: SQLiteCloudConnect, dbname: str, filename: str) -> None:
"""
Download a database from the server.
Raises:
- SQCloudException: If an error occurs while downloading the database.
+ SQLiteCloudException: If an error occurs while downloading the database.
"""
driver = Driver()
diff --git a/src/sqlitecloud/driver.py b/src/sqlitecloud/driver.py
index c1843dd..f951d68 100644
--- a/src/sqlitecloud/driver.py
+++ b/src/sqlitecloud/driver.py
@@ -1,56 +1,58 @@
+import json
import logging
import select
import socket
import ssl
import threading
from io import BufferedReader, BufferedWriter
-from typing import Callable, Optional, Union
+from typing import Callable, Dict, Optional, Tuple, Union
import lz4.block
-from sqlitecloud.resultset import SQCloudResult, SqliteCloudResultSet
+from sqlitecloud.resultset import SQLiteCloudResult, SQLiteCloudResultSet
from sqlitecloud.types import (
- SQCLOUD_CMD,
- SQCLOUD_DEFAULT,
- SQCLOUD_INTERNAL_ERRCODE,
- SQCLOUD_RESULT_TYPE,
- SQCLOUD_ROWSET,
- SQCloudConfig,
- SQCloudConnect,
- SQCloudException,
- SQCloudNumber,
- SQCloudRowsetSignature,
- SQCloudValue,
+ SQLITECLOUD_CMD,
+ SQLITECLOUD_DEFAULT,
+ SQLITECLOUD_INTERNAL_ERRCODE,
+ SQLITECLOUD_RESULT_TYPE,
+ SQLITECLOUD_ROWSET,
+ SQLiteCloudConfig,
+ SQLiteCloudConnect,
+ SQLiteCloudDataTypes,
+ SQLiteCloudException,
+ SQLiteCloudNumber,
+ SQLiteCloudRowsetSignature,
+ SQLiteCloudValue,
)
class Driver:
- SQCLOUD_DEFAULT_UPLOAD_SIZE = 512 * 1024
+ SQLiteCloud_DEFAULT_UPLOAD_SIZE = 512 * 1024
def __init__(self) -> None:
# Used while parsing chunked rowset
- self._rowset: SQCloudResult = None
+ self._rowset: SQLiteCloudResult = None
def connect(
- self, hostname: str, port: int, config: SQCloudConfig
- ) -> SQCloudConnect:
+ self, hostname: str, port: int, config: SQLiteCloudConfig
+ ) -> SQLiteCloudConnect:
"""
Connect to the SQLite Cloud server.
Args:
hostname (str): The hostname of the server.
port (int): The port number of the server.
- config (SQCloudConfig): The configuration for the connection.
+ config (SQLiteCloudConfig): The configuration for the connection.
Returns:
- SQCloudConnect: The connection object.
+ SQLiteCloudConnect: The connection object.
Raises:
- SQCloudException: If an error occurs while connecting the socket.
+ SQLiteCloudException: If an error occurs while connecting the socket.
"""
sock = self._internal_connect(hostname, port, config)
- connection = SQCloudConnect()
+ connection = SQLiteCloudConnect()
connection.config = config
connection.socket = sock
@@ -58,7 +60,9 @@ def connect(
return connection
- def disconnect(self, conn: SQCloudConnect, only_main_socket: bool = False) -> None:
+ def disconnect(
+ self, conn: SQLiteCloudConnect, only_main_socket: bool = False
+ ) -> None:
"""
Disconnect from the SQLite Cloud server.
"""
@@ -72,13 +76,15 @@ def disconnect(self, conn: SQCloudConnect, only_main_socket: bool = False) -> No
if not only_main_socket:
conn.pubsub_socket = None
- def execute(self, command: str, connection: SQCloudConnect) -> SQCloudResult:
+ def execute(
+ self, command: str, connection: SQLiteCloudConnect
+ ) -> SQLiteCloudResult:
"""
Execute a query on the SQLite Cloud server.
"""
return self._internal_run_command(connection, command)
- def send_blob(self, blob: bytes, conn: SQCloudConnect) -> SQCloudResult:
+ def send_blob(self, blob: bytes, conn: SQLiteCloudConnect) -> SQLiteCloudResult:
"""
Send a blob to the SQLite Cloud server.
"""
@@ -88,8 +94,27 @@ def send_blob(self, blob: bytes, conn: SQCloudConnect) -> SQCloudResult:
finally:
conn.isblob = False
+ def prepare_statement(
+ self,
+ query: str,
+ parameters: Union[
+ Tuple[SQLiteCloudDataTypes], Dict[Union[str, int], SQLiteCloudDataTypes]
+ ],
+ ) -> str:
+ # If parameters is a dictionary, replace the keys in the query with the values
+ if isinstance(parameters, dict):
+ for key, value in parameters.items():
+ query = query.replace(":" + str(key), self.escape_sql_parameter(value))
+
+ # If parameters is a tuple, replace each '?' in the query with a value from the tuple
+ elif isinstance(parameters, tuple):
+ for value in parameters:
+ query = query.replace("?", self.escape_sql_parameter(value), 1)
+
+ return query
+
def is_connected(
- self, connection: SQCloudConnect, main_socket: bool = True
+ self, connection: SQLiteCloudConnect, main_socket: bool = True
) -> bool:
"""
Check if the connection is still open.
@@ -105,8 +130,35 @@ def is_connected(
return True
+ def escape_sql_parameter(self, param):
+ if param is None or param is None:
+ return "NULL"
+
+ if isinstance(param, bool):
+ return "1" if param else "0"
+
+ if isinstance(param, str):
+ # replace single quote with two single quotes
+ param = param.replace("'", "''")
+ return f"'{param}'"
+
+ if isinstance(param, (int, float)):
+ return str(param)
+
+ # serialize buffer as X'...' hex encoded string
+ if isinstance(param, bytes):
+ return f"X'{param.hex()}'"
+
+ if isinstance(param, dict) or isinstance(param, list):
+ # serialize json then escape single quotes
+ json_string = json.dumps(param)
+ json_string = json_string.replace("'", "''")
+ return f"'{json_string}'"
+
+ raise SQLiteCloudException(f"Unsupported parameter type: {type(param)}")
+
def _internal_connect(
- self, hostname: str, port: int, config: SQCloudConfig
+ self, hostname: str, port: int, config: SQLiteCloudConfig
) -> socket:
"""
Create a socket connection to the SQLite Cloud server.
@@ -132,14 +184,16 @@ def _internal_connect(
sock.connect((hostname, port))
except Exception as e:
errmsg = "An error occurred while initializing the socket."
- raise SQCloudException(errmsg) from e
+ raise SQLiteCloudException(errmsg) from e
return sock
def _internal_reconnect(self, buffer: bytes) -> bool:
return True
- def _internal_setup_pubsub(self, connection: SQCloudConnect, buffer: bytes) -> bool:
+ def _internal_setup_pubsub(
+ self, connection: SQLiteCloudConnect, buffer: bytes
+ ) -> bool:
"""
Prepare the connection for PubSub.
Opens a new specific socket and starts the thread to listen for incoming messages.
@@ -148,7 +202,7 @@ def _internal_setup_pubsub(self, connection: SQCloudConnect, buffer: bytes) -> b
return True
if connection.pubsub_callback is None:
- raise SQCloudException(
+ raise SQLiteCloudException(
"A callback function must be provided to setup the PubSub connection."
)
@@ -169,7 +223,7 @@ def _internal_setup_pubsub(self, connection: SQCloudConnect, buffer: bytes) -> b
return True
- def _internal_pubsub_thread(self, connection: SQCloudConnect) -> None:
+ def _internal_pubsub_thread(self, connection: SQLiteCloudConnect) -> None:
blen = 2048
buffer: bytes = b""
@@ -199,7 +253,7 @@ def _internal_pubsub_thread(self, connection: SQCloudConnect) -> None:
break
except Exception as e:
logging.error(
- f"An error occurred while reading data: {SQCLOUD_INTERNAL_ERRCODE.NETWORK.value} ({e})."
+ f"An error occurred while reading data: {SQLITECLOUD_INTERNAL_ERRCODE.NETWORK.value} ({e})."
)
break
@@ -208,24 +262,24 @@ def _internal_pubsub_thread(self, connection: SQCloudConnect) -> None:
blen -= nread
buffer += data
- sqcloud_number = self._internal_parse_number(buffer)
- clen = sqcloud_number.value
+ SQLiteCloud_number = self._internal_parse_number(buffer)
+ clen = SQLiteCloud_number.value
if clen == 0:
continue
# check if read is complete
# clen is the lenght parsed in the buffer
# cstart is the index of the first space
- cstart = sqcloud_number.cstart
+ cstart = SQLiteCloud_number.cstart
if clen + cstart != tread:
continue
result = self._internal_parse_buffer(connection, buffer, tread)
- if result.tag == SQCLOUD_RESULT_TYPE.RESULT_STRING:
- result.tag = SQCLOUD_RESULT_TYPE.RESULT_JSON
+ if result.tag == SQLITECLOUD_RESULT_TYPE.RESULT_STRING:
+ result.tag = SQLITECLOUD_RESULT_TYPE.RESULT_JSON
connection.pubsub_callback(
- connection, SqliteCloudResultSet(result), connection.pubsub_data
+ connection, SQLiteCloudResultSet(result), connection.pubsub_data
)
except Exception as e:
logging.error(f"An error occurred while parsing data: {e}.")
@@ -235,7 +289,7 @@ def _internal_pubsub_thread(self, connection: SQCloudConnect) -> None:
def upload_database(
self,
- connection: SQCloudConnect,
+ connection: SQLiteCloudConnect,
dbname: str,
key: Optional[str],
is_file_transfer: bool,
@@ -249,7 +303,7 @@ def upload_database(
Uploads a database to the server.
Args:
- connection (SQCloudConnect): The connection object to the SQLite Cloud server.
+ connection (SQLiteCloudConnect): The connection object to the SQLite Cloud server.
dbname (str): The name of the database to upload.
key (Optional[str]): The encryption key for the database, if applicable.
is_file_transfer (bool): Indicates whether the database is being transferred as a file.
@@ -260,7 +314,7 @@ def upload_database(
xCallback (Callable[[BufferedReader, int, int, int], bytes]): The callback function to read the buffer.
Raises:
- SQCloudException: If an error occurs during the upload process.
+ SQLiteCloudException: If an error occurs during the upload process.
"""
keyarg = "KEY " if key else ""
@@ -277,7 +331,7 @@ def upload_database(
# execute command on server side
result = self._internal_run_command(connection, command)
if not result.data[0]:
- raise SQCloudException(
+ raise SQLiteCloudException(
"An error occurred while initializing the upload of the database."
)
@@ -287,12 +341,12 @@ def upload_database(
try:
while True:
# execute callback to read buffer
- blen = SQCLOUD_DEFAULT.UPLOAD_SIZE.value
+ blen = SQLITECLOUD_DEFAULT.UPLOAD_SIZE.value
try:
buffer = xCallback(fd, blen, dbsize, nprogress)
blen = len(buffer)
except Exception as e:
- raise SQCloudException(
+ raise SQLiteCloudException(
"An error occurred while reading the file."
) from e
@@ -300,7 +354,7 @@ def upload_database(
# send also the final confirmation blob of zero bytes
self.send_blob(buffer, connection)
except Exception as e:
- raise SQCloudException(
+ raise SQLiteCloudException(
"An error occurred while uploading the file."
) from e
@@ -316,7 +370,7 @@ def upload_database(
def download_database(
self,
- connection: SQCloudConnect,
+ connection: SQLiteCloudConnect,
dbname: str,
fd: BufferedWriter,
xCallback: Callable[[BufferedWriter, int, int, int], bytes],
@@ -326,14 +380,14 @@ def download_database(
Downloads a database from the SQLite Cloud service.
Args:
- connection (SQCloudConnect): The connection object used to communicate with the SQLite Cloud service.
+ connection (SQLiteCloudConnect): The connection object used to communicate with the SQLite Cloud service.
dbname (str): The name of the database to download.
fd (BufferedWriter): The file descriptor to write the downloaded data to.
xCallback (Callable[[BufferedWriter, int, int, int], bytes]): A callback function to write downloaded data with the download progress information.
if_exists (bool): If True, the download won't rise an exception if database is missing.
Raises:
- SQCloudException: If an error occurs while downloading the database.
+ SQLiteCloudException: If an error occurs while downloading the database.
"""
exists_cmd = " IF EXISTS" if if_exists else ""
@@ -342,7 +396,7 @@ def download_database(
)
if result.nrows == 0:
- raise SQCloudException(
+ raise SQLiteCloudException(
"An error occurred while initializing the download of the database."
)
@@ -373,13 +427,17 @@ def download_database(
raise e
def _internal_config_apply(
- self, connection: SQCloudConnect, config: SQCloudConfig
+ self, connection: SQLiteCloudConnect, config: SQLiteCloudConfig
) -> None:
if config.timeout > 0:
connection.socket.settimeout(config.timeout)
buffer = ""
+ # it must be executed before authentication command
+ if config.non_linearizable:
+ buffer += "SET CLIENT KEY NONLINEARIZABLE TO 1;"
+
if config.account.apikey:
buffer += f"AUTH APIKEY {config.account.apikey};"
@@ -398,9 +456,6 @@ def _internal_config_apply(
if config.zerotext:
buffer += "SET CLIENT KEY ZEROTEXT TO 1;"
- if config.non_linearizable:
- buffer += "SET CLIENT KEY NONLINEARIZABLE TO 1;"
-
if config.noblob:
buffer += "SET CLIENT KEY NOBLOB TO 1;"
@@ -418,16 +473,22 @@ def _internal_config_apply(
def _internal_run_command(
self,
- connection: SQCloudConnect,
+ connection: SQLiteCloudConnect,
command: Union[str, bytes],
main_socket: bool = True,
- ) -> SQCloudResult:
+ ) -> SQLiteCloudResult:
+ if not self.is_connected(connection, main_socket):
+ raise SQLiteCloudException(
+ "The connection is closed.",
+ SQLITECLOUD_INTERNAL_ERRCODE.NETWORK,
+ )
+
self._internal_socket_write(connection, command, main_socket)
return self._internal_socket_read(connection, main_socket)
def _internal_socket_write(
self,
- connection: SQCloudConnect,
+ connection: SQLiteCloudConnect,
command: Union[str, bytes],
main_socket: bool = True,
) -> None:
@@ -443,9 +504,9 @@ def _internal_socket_write(
try:
sock.sendall(header.encode())
except Exception as exc:
- raise SQCloudException(
+ raise SQLiteCloudException(
"An error occurred while writing header data.",
- SQCLOUD_INTERNAL_ERRCODE.NETWORK,
+ SQLITECLOUD_INTERNAL_ERRCODE.NETWORK,
) from exc
# write buffer
@@ -454,14 +515,14 @@ def _internal_socket_write(
try:
sock.sendall(buffer)
except Exception as exc:
- raise SQCloudException(
+ raise SQLiteCloudException(
"An error occurred while writing data.",
- SQCLOUD_INTERNAL_ERRCODE.NETWORK,
+ SQLITECLOUD_INTERNAL_ERRCODE.NETWORK,
) from exc
def _internal_socket_read(
- self, connection: SQCloudConnect, main_socket: bool = True
- ) -> SQCloudResult:
+ self, connection: SQLiteCloudConnect, main_socket: bool = True
+ ) -> SQLiteCloudResult:
"""
Read from the socket and parse the response.
@@ -480,11 +541,11 @@ def _internal_socket_read(
try:
data = sock.recv(buffer_size)
if not data:
- raise SQCloudException("Incomplete response from server.")
+ raise SQLiteCloudException("Incomplete response from server.")
except Exception as exc:
- raise SQCloudException(
+ raise SQLiteCloudException(
"An error occurred while reading data from the socket.",
- SQCLOUD_INTERNAL_ERRCODE.NETWORK,
+ SQLITECLOUD_INTERNAL_ERRCODE.NETWORK,
) from exc
# the expected data length to read
@@ -496,23 +557,23 @@ def _internal_socket_read(
c = chr(buffer[0])
if (
- c == SQCLOUD_CMD.INT.value
- or c == SQCLOUD_CMD.FLOAT.value
- or c == SQCLOUD_CMD.NULL.value
+ c == SQLITECLOUD_CMD.INT.value
+ or c == SQLITECLOUD_CMD.FLOAT.value
+ or c == SQLITECLOUD_CMD.NULL.value
):
if not buffer.endswith(b" "):
continue
- elif c == SQCLOUD_CMD.ROWSET_CHUNK.value:
- isEndOfChunk = buffer.endswith(SQCLOUD_ROWSET.CHUNKS_END.value)
+ elif c == SQLITECLOUD_CMD.ROWSET_CHUNK.value:
+ isEndOfChunk = buffer.endswith(SQLITECLOUD_ROWSET.CHUNKS_END.value)
if not isEndOfChunk:
continue
else:
- sqcloud_number = self._internal_parse_number(buffer)
- n = sqcloud_number.value
- cstart = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer)
+ n = SQLiteCloud_number.value
+ cstart = SQLiteCloud_number.cstart
can_be_zerolength = (
- c == SQCLOUD_CMD.BLOB.value or c == SQCLOUD_CMD.STRING.value
+ c == SQLITECLOUD_CMD.BLOB.value or c == SQLITECLOUD_CMD.STRING.value
)
if n == 0 and not can_be_zerolength:
continue
@@ -521,9 +582,11 @@ def _internal_socket_read(
return self._internal_parse_buffer(connection, buffer, len(buffer))
- def _internal_parse_number(self, buffer: bytes, index: int = 1) -> SQCloudNumber:
- sqcloud_number = SQCloudNumber()
- sqcloud_number.value = 0
+ def _internal_parse_number(
+ self, buffer: bytes, index: int = 1
+ ) -> SQLiteCloudNumber:
+ SQLiteCloud_number = SQLiteCloudNumber()
+ SQLiteCloud_number.value = 0
extvalue = 0
isext = False
blen = len(buffer)
@@ -539,9 +602,9 @@ def _internal_parse_number(self, buffer: bytes, index: int = 1) -> SQCloudNumber
# check for end of value
if c == " ":
- sqcloud_number.cstart = i + 1
- sqcloud_number.extcode = extvalue
- return sqcloud_number
+ SQLiteCloud_number.cstart = i + 1
+ SQLiteCloud_number.extcode = extvalue
+ return SQLiteCloud_number
val = int(c) if c.isdigit() else 0
@@ -549,14 +612,14 @@ def _internal_parse_number(self, buffer: bytes, index: int = 1) -> SQCloudNumber
if isext:
extvalue = (extvalue * 10) + val
else:
- sqcloud_number.value = (sqcloud_number.value * 10) + val
+ SQLiteCloud_number.value = (SQLiteCloud_number.value * 10) + val
- sqcloud_number.value = 0
- return sqcloud_number
+ SQLiteCloud_number.value = 0
+ return SQLiteCloud_number
def _internal_parse_buffer(
- self, connection: SQCloudConnect, buffer: bytes, blen: int
- ) -> SQCloudResult:
+ self, connection: SQLiteCloudConnect, buffer: bytes, blen: int
+ ) -> SQLiteCloudResult:
# possible return values:
# True => OK
# False => error
@@ -569,15 +632,15 @@ def _internal_parse_buffer(
# check OK value
if buffer == b"+2 OK":
- return SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_OK, True)
+ return SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_OK, True)
cmd = chr(buffer[0])
# check for compressed result
- if cmd == SQCLOUD_CMD.COMPRESSED.value:
+ if cmd == SQLITECLOUD_CMD.COMPRESSED.value:
buffer = self._internal_uncompress_data(buffer)
if buffer is None:
- raise SQCloudException(
+ raise SQLiteCloudException(
f"An error occurred while decompressing the input buffer of len {blen}."
)
@@ -587,53 +650,54 @@ def _internal_parse_buffer(
# first character contains command type
if cmd in [
- SQCLOUD_CMD.ZEROSTRING.value,
- SQCLOUD_CMD.RECONNECT.value,
- SQCLOUD_CMD.PUBSUB.value,
- SQCLOUD_CMD.COMMAND.value,
- SQCLOUD_CMD.STRING.value,
- SQCLOUD_CMD.ARRAY.value,
- SQCLOUD_CMD.BLOB.value,
- SQCLOUD_CMD.JSON.value,
+ SQLITECLOUD_CMD.ZEROSTRING.value,
+ SQLITECLOUD_CMD.RECONNECT.value,
+ SQLITECLOUD_CMD.PUBSUB.value,
+ SQLITECLOUD_CMD.COMMAND.value,
+ SQLITECLOUD_CMD.STRING.value,
+ SQLITECLOUD_CMD.ARRAY.value,
+ SQLITECLOUD_CMD.BLOB.value,
+ SQLITECLOUD_CMD.JSON.value,
]:
sqlite_number = self._internal_parse_number(buffer)
len_ = sqlite_number.value
cstart = sqlite_number.cstart
if len_ == 0:
- return SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_STRING, "")
+ return SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_STRING, "")
tag = (
- SQCLOUD_RESULT_TYPE.RESULT_JSON
- if cmd == SQCLOUD_CMD.JSON.value
- else SQCLOUD_RESULT_TYPE.RESULT_STRING
+ SQLITECLOUD_RESULT_TYPE.RESULT_JSON
+ if cmd == SQLITECLOUD_CMD.JSON.value
+ else SQLITECLOUD_RESULT_TYPE.RESULT_STRING
)
- if cmd == SQCLOUD_CMD.ZEROSTRING.value:
+ if cmd == SQLITECLOUD_CMD.ZEROSTRING.value:
len_ -= 1
clone = buffer[cstart : cstart + len_]
- if cmd == SQCLOUD_CMD.COMMAND.value:
+ if cmd == SQLITECLOUD_CMD.COMMAND.value:
return self._internal_run_command(connection, clone)
- elif cmd == SQCLOUD_CMD.PUBSUB.value:
- return SQCloudResult(
- SQCLOUD_RESULT_TYPE.RESULT_OK,
+ elif cmd == SQLITECLOUD_CMD.PUBSUB.value:
+ return SQLiteCloudResult(
+ SQLITECLOUD_RESULT_TYPE.RESULT_OK,
self._internal_setup_pubsub(connection, clone),
)
- elif cmd == SQCLOUD_CMD.RECONNECT.value:
- return SQCloudResult(
- SQCLOUD_RESULT_TYPE.RESULT_OK, self._internal_reconnect(clone)
+ elif cmd == SQLITECLOUD_CMD.RECONNECT.value:
+ return SQLiteCloudResult(
+ SQLITECLOUD_RESULT_TYPE.RESULT_OK, self._internal_reconnect(clone)
)
- elif cmd == SQCLOUD_CMD.ARRAY.value:
- return SQCloudResult(
- SQCLOUD_RESULT_TYPE.RESULT_ARRAY, self._internal_parse_array(clone)
+ elif cmd == SQLITECLOUD_CMD.ARRAY.value:
+ return SQLiteCloudResult(
+ SQLITECLOUD_RESULT_TYPE.RESULT_ARRAY,
+ self._internal_parse_array(clone),
)
- elif cmd == SQCLOUD_CMD.BLOB.value:
- tag = SQCLOUD_RESULT_TYPE.RESULT_BLOB
+ elif cmd == SQLITECLOUD_CMD.BLOB.value:
+ tag = SQLITECLOUD_RESULT_TYPE.RESULT_BLOB
- clone = clone.decode() if cmd != SQCLOUD_CMD.BLOB.value else clone
- return SQCloudResult(tag, clone)
+ clone = clone.decode() if cmd != SQLITECLOUD_CMD.BLOB.value else clone
+ return SQLiteCloudResult(tag, clone)
- elif cmd == SQCLOUD_CMD.ERROR.value:
+ elif cmd == SQLITECLOUD_CMD.ERROR.value:
# -LEN ERRCODE:EXTCODE ERRMSG
sqlite_number = self._internal_parse_number(buffer)
len_ = sqlite_number.value
@@ -649,9 +713,9 @@ def _internal_parse_buffer(
len_ -= cstart2
errmsg = clone[cstart2:]
- raise SQCloudException(errmsg.decode(), errcode, xerrcode)
+ raise SQLiteCloudException(errmsg.decode(), errcode, xerrcode)
- elif cmd in [SQCLOUD_CMD.ROWSET.value, SQCLOUD_CMD.ROWSET_CHUNK.value]:
+ elif cmd in [SQLITECLOUD_CMD.ROWSET.value, SQLITECLOUD_CMD.ROWSET_CHUNK.value]:
# CMD_ROWSET: *LEN 0:VERSION ROWS COLS DATA
# - When decompressed, LEN for ROWSET is *0
#
@@ -659,7 +723,7 @@ def _internal_parse_buffer(
#
rowset_signature = self._internal_parse_rowset_signature(buffer)
if rowset_signature.start < 0:
- raise SQCloudException("Cannot parse rowset signature")
+ raise SQLiteCloudException("Cannot parse rowset signature")
# check for end-of-chunk condition
if rowset_signature.start == 0 and rowset_signature.version == 0:
@@ -679,35 +743,35 @@ def _internal_parse_buffer(
# continue parsing next chunk in the buffer
sign_len = rowset_signature.len
buffer = buffer[sign_len + len(f"/{sign_len} ") :]
- if cmd == SQCLOUD_CMD.ROWSET_CHUNK.value and buffer:
+ if cmd == SQLITECLOUD_CMD.ROWSET_CHUNK.value and buffer:
return self._internal_parse_buffer(connection, buffer, len(buffer))
return rowset
- elif cmd == SQCLOUD_CMD.NULL.value:
- return SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_NONE, None)
+ elif cmd == SQLITECLOUD_CMD.NULL.value:
+ return SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_NONE, None)
- elif cmd in [SQCLOUD_CMD.INT.value, SQCLOUD_CMD.FLOAT.value]:
- sqcloud_value = self._internal_parse_value(buffer)
- clone = sqcloud_value.value
+ elif cmd in [SQLITECLOUD_CMD.INT.value, SQLITECLOUD_CMD.FLOAT.value]:
+ SQLiteCloud_value = self._internal_parse_value(buffer)
+ clone = SQLiteCloud_value.value
tag = (
- SQCLOUD_RESULT_TYPE.RESULT_INTEGER
- if cmd == SQCLOUD_CMD.INT.value
- else SQCLOUD_RESULT_TYPE.RESULT_FLOAT
+ SQLITECLOUD_RESULT_TYPE.RESULT_INTEGER
+ if cmd == SQLITECLOUD_CMD.INT.value
+ else SQLITECLOUD_RESULT_TYPE.RESULT_FLOAT
)
if clone is None:
- return SQCloudResult(tag, 0)
+ return SQLiteCloudResult(tag, 0)
- if cmd == SQCLOUD_CMD.INT.value:
- return SQCloudResult(tag, int(clone))
- return SQCloudResult(tag, float(clone))
+ if cmd == SQLITECLOUD_CMD.INT.value:
+ return SQLiteCloudResult(tag, int(clone))
+ return SQLiteCloudResult(tag, float(clone))
- elif cmd == SQCLOUD_CMD.RAWJSON.value:
- return SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_NONE, None)
+ elif cmd == SQLITECLOUD_CMD.RAWJSON.value:
+ return SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_NONE, None)
- return SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_NONE, None)
+ return SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_NONE, None)
def _internal_uncompress_data(self, buffer: bytes) -> Optional[bytes]:
"""
@@ -757,62 +821,64 @@ def _internal_parse_array(self, buffer: bytes) -> list:
r: str = []
for i in range(n):
- sqcloud_value = self._internal_parse_value(buffer, start)
- start += sqcloud_value.cellsize
- r.append(sqcloud_value.value)
+ SQLiteCloud_value = self._internal_parse_value(buffer, start)
+ start += SQLiteCloud_value.cellsize
+ r.append(SQLiteCloud_value.value)
return r
- def _internal_parse_value(self, buffer: bytes, index: int = 0) -> SQCloudValue:
- sqcloud_value = SQCloudValue()
+ def _internal_parse_value(self, buffer: bytes, index: int = 0) -> SQLiteCloudValue:
+ SQLiteCloud_value = SQLiteCloudValue()
len = 0
cellsize = 0
# handle special NULL value case
c = chr(buffer[index])
- if buffer is None or c == SQCLOUD_CMD.NULL.value:
+ if buffer is None or c == SQLITECLOUD_CMD.NULL.value:
len = 0
if cellsize is not None:
cellsize = 2
- sqcloud_value.len = len
- sqcloud_value.cellsize = cellsize
+ SQLiteCloud_value.len = len
+ SQLiteCloud_value.cellsize = cellsize
- return sqcloud_value
+ return SQLiteCloud_value
- sqcloud_number = self._internal_parse_number(buffer, index + 1)
- blen = sqcloud_number.value
- cstart = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, index + 1)
+ blen = SQLiteCloud_number.value
+ cstart = SQLiteCloud_number.cstart
# handle decimal/float cases
- if c == SQCLOUD_CMD.INT.value or c == SQCLOUD_CMD.FLOAT.value:
+ if c == SQLITECLOUD_CMD.INT.value or c == SQLITECLOUD_CMD.FLOAT.value:
nlen = cstart - index
len = nlen - 2
cellsize = nlen
- sqcloud_value.value = (buffer[index + 1 : index + 1 + len]).decode()
- sqcloud_value.len
- sqcloud_value.cellsize = cellsize
+ SQLiteCloud_value.value = (buffer[index + 1 : index + 1 + len]).decode()
+ SQLiteCloud_value.len
+ SQLiteCloud_value.cellsize = cellsize
- return sqcloud_value
+ return SQLiteCloud_value
- len = blen - 1 if c == SQCLOUD_CMD.ZEROSTRING.value else blen
+ len = blen - 1 if c == SQLITECLOUD_CMD.ZEROSTRING.value else blen
cellsize = blen + cstart - index
- sqcloud_value.value = (buffer[cstart : cstart + len]).decode()
- sqcloud_value.len = len
- sqcloud_value.cellsize = cellsize
+ SQLiteCloud_value.value = (buffer[cstart : cstart + len]).decode()
+ SQLiteCloud_value.len = len
+ SQLiteCloud_value.cellsize = cellsize
- return sqcloud_value
+ return SQLiteCloud_value
- def _internal_parse_rowset_signature(self, buffer: bytes) -> SQCloudRowsetSignature:
+ def _internal_parse_rowset_signature(
+ self, buffer: bytes
+ ) -> SQLiteCloudRowsetSignature:
# ROWSET: *LEN 0:VERS NROWS NCOLS DATA
# ROWSET in CHUNK: /LEN IDX:VERS NROWS NCOLS DATA
- signature = SQCloudRowsetSignature()
+ signature = SQLiteCloudRowsetSignature()
# check for end-of-chunk condition
- if buffer == SQCLOUD_ROWSET.CHUNKS_END.value:
+ if buffer == SQLITECLOUD_ROWSET.CHUNKS_END.value:
signature.version = 0
signature.start = 0
return signature
@@ -844,21 +910,21 @@ def _internal_parse_rowset_signature(self, buffer: bytes) -> SQCloudRowsetSignat
return signature
else:
- return SQCloudRowsetSignature()
- return SQCloudRowsetSignature()
+ return SQLiteCloudRowsetSignature()
+ return SQLiteCloudRowsetSignature()
def _internal_parse_rowset(
self, buffer: bytes, start: int, idx: int, version: int, nrows: int, ncols: int
- ) -> SQCloudResult:
+ ) -> SQLiteCloudResult:
rowset = None
n = start
- ischunk = chr(buffer[0]) == SQCLOUD_CMD.ROWSET_CHUNK.value
+ ischunk = chr(buffer[0]) == SQLITECLOUD_CMD.ROWSET_CHUNK.value
# idx == 0 means first (and only) chunk for rowset
# idx == 1 means first chunk for chunked rowset
first_chunk = (ischunk and idx == 1) or (not ischunk and idx == 0)
if first_chunk:
- rowset = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_ROWSET)
+ rowset = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
rowset.nrows = nrows
rowset.ncols = ncols
rowset.version = version
@@ -867,7 +933,7 @@ def _internal_parse_rowset(
self._rowset = rowset
n = self._internal_parse_rowset_header(rowset, buffer, start)
if n <= 0:
- raise SQCloudException("Cannot parse rowset header")
+ raise SQLiteCloudException("Cannot parse rowset header")
else:
rowset = self._rowset
rowset.nrows += nrows
@@ -878,16 +944,16 @@ def _internal_parse_rowset(
return rowset
def _internal_parse_rowset_header(
- self, rowset: SQCloudResult, buffer: bytes, start: int
+ self, rowset: SQLiteCloudResult, buffer: bytes, start: int
) -> int:
ncols = rowset.ncols
# parse column names
rowset.colname = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- number_len = sqcloud_number.value
- cstart = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ number_len = SQLiteCloud_number.value
+ cstart = SQLiteCloud_number.cstart
value = buffer[cstart : cstart + number_len]
rowset.colname.append(value.decode())
start = cstart + number_len
@@ -896,14 +962,16 @@ def _internal_parse_rowset_header(
return start
if rowset.version != 2:
- raise SQCloudException(f"Rowset version {rowset.version} is not supported.")
+ raise SQLiteCloudException(
+ f"Rowset version {rowset.version} is not supported."
+ )
# parse declared types
rowset.decltype = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- number_len = sqcloud_number.value
- cstart = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ number_len = SQLiteCloud_number.value
+ cstart = SQLiteCloud_number.cstart
value = buffer[cstart : cstart + number_len]
rowset.decltype.append(value.decode())
start = cstart + number_len
@@ -911,9 +979,9 @@ def _internal_parse_rowset_header(
# parse database names
rowset.dbname = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- number_len = sqcloud_number.value
- cstart = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ number_len = SQLiteCloud_number.value
+ cstart = SQLiteCloud_number.cstart
value = buffer[cstart : cstart + number_len]
rowset.dbname.append(value.decode())
start = cstart + number_len
@@ -921,9 +989,9 @@ def _internal_parse_rowset_header(
# parse table names
rowset.tblname = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- number_len = sqcloud_number.value
- cstart = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ number_len = SQLiteCloud_number.value
+ cstart = SQLiteCloud_number.cstart
value = buffer[cstart : cstart + number_len]
rowset.tblname.append(value.decode())
start = cstart + number_len
@@ -931,9 +999,9 @@ def _internal_parse_rowset_header(
# parse column original names
rowset.origname = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- number_len = sqcloud_number.value
- cstart = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ number_len = SQLiteCloud_number.value
+ cstart = SQLiteCloud_number.cstart
value = buffer[cstart : cstart + number_len]
rowset.origname.append(value.decode())
start = cstart + number_len
@@ -941,31 +1009,31 @@ def _internal_parse_rowset_header(
# parse not null flags
rowset.notnull = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- rowset.notnull.append(sqcloud_number.value)
- start = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ rowset.notnull.append(SQLiteCloud_number.value)
+ start = SQLiteCloud_number.cstart
# parse primary key flags
rowset.prikey = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- rowset.prikey.append(sqcloud_number.value)
- start = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ rowset.prikey.append(SQLiteCloud_number.value)
+ start = SQLiteCloud_number.cstart
# parse autoincrement flags
rowset.autoinc = []
for i in range(ncols):
- sqcloud_number = self._internal_parse_number(buffer, start)
- rowset.autoinc.append(sqcloud_number.value)
- start = sqcloud_number.cstart
+ SQLiteCloud_number = self._internal_parse_number(buffer, start)
+ rowset.autoinc.append(SQLiteCloud_number.value)
+ start = SQLiteCloud_number.cstart
return start
def _internal_parse_rowset_values(
- self, rowset: SQCloudResult, buffer: bytes, start: int, bound: int
+ self, rowset: SQLiteCloudResult, buffer: bytes, start: int, bound: int
):
# loop to parse each individual value
for i in range(bound):
- sqcloud_value = self._internal_parse_value(buffer, start)
- start += sqcloud_value.cellsize
- rowset.data.append(sqcloud_value.value)
+ SQLiteCloud_value = self._internal_parse_value(buffer, start)
+ start += SQLiteCloud_value.cellsize
+ rowset.data.append(SQLiteCloud_value.value)
diff --git a/src/sqlitecloud/pubsub.py b/src/sqlitecloud/pubsub.py
index 39277cf..7999908 100644
--- a/src/sqlitecloud/pubsub.py
+++ b/src/sqlitecloud/pubsub.py
@@ -1,21 +1,21 @@
from typing import Callable, Optional
from sqlitecloud.driver import Driver
-from sqlitecloud.resultset import SqliteCloudResultSet
-from sqlitecloud.types import SQCLOUD_PUBSUB_SUBJECT, SQCloudConnect
+from sqlitecloud.resultset import SQLiteCloudResultSet
+from sqlitecloud.types import SQLITECLOUD_PUBSUB_SUBJECT, SQLiteCloudConnect
-class SqliteCloudPubSub:
+class SQLiteCloudPubSub:
def __init__(self) -> None:
self._driver = Driver()
def listen(
self,
- connection: SQCloudConnect,
- subject_type: SQCLOUD_PUBSUB_SUBJECT,
+ connection: SQLiteCloudConnect,
+ subject_type: SQLITECLOUD_PUBSUB_SUBJECT,
subject_name: str,
callback: Callable[
- [SQCloudConnect, Optional[SqliteCloudResultSet], Optional[any]], None
+ [SQLiteCloudConnect, Optional[SQLiteCloudResultSet], Optional[any]], None
],
data: Optional[any] = None,
) -> None:
@@ -28,8 +28,8 @@ def listen(
def unlisten(
self,
- connection: SQCloudConnect,
- subject_type: SQCLOUD_PUBSUB_SUBJECT,
+ connection: SQLiteCloudConnect,
+ subject_type: SQLITECLOUD_PUBSUB_SUBJECT,
subject_name: str,
) -> None:
subject = "TABLE " if subject_type.value == "TABLE" else ""
@@ -40,17 +40,19 @@ def unlisten(
connection.pubsub_data = None
def create_channel(
- self, connection: SQCloudConnect, name: str, if_not_exists: bool = False
+ self, connection: SQLiteCloudConnect, name: str, if_not_exists: bool = False
) -> None:
if if_not_exists:
self._driver.execute(f"CREATE CHANNEL {name} IF NOT EXISTS;", connection)
else:
self._driver.execute(f"CREATE CHANNEL {name};", connection)
- def notify_channel(self, connection: SQCloudConnect, name: str, data: str) -> None:
+ def notify_channel(
+ self, connection: SQLiteCloudConnect, name: str, data: str
+ ) -> None:
self._driver.execute(f"NOTIFY {name} '{data}';", connection)
- def set_pubsub_only(self, connection: SQCloudConnect) -> None:
+ def set_pubsub_only(self, connection: SQLiteCloudConnect) -> None:
"""
Close the main socket, leaving only the pub/sub socket opened and ready
to receive incoming notifications from subscripted channels and tables.
@@ -60,10 +62,10 @@ def set_pubsub_only(self, connection: SQCloudConnect) -> None:
self._driver.execute("PUBSUB ONLY;", connection)
self._driver.disconnect(connection, only_main_socket=True)
- def is_connected(self, connection: SQCloudConnect) -> bool:
+ def is_connected(self, connection: SQLiteCloudConnect) -> bool:
return self._driver.is_connected(connection, False)
- def list_connections(self, connection: SQCloudConnect) -> SqliteCloudResultSet:
- return SqliteCloudResultSet(
+ def list_connections(self, connection: SQLiteCloudConnect) -> SQLiteCloudResultSet:
+ return SQLiteCloudResultSet(
self._driver.execute("LIST PUBSUB CONNECTIONS;", connection)
)
diff --git a/src/sqlitecloud/resultset.py b/src/sqlitecloud/resultset.py
index 0220665..c6160d7 100644
--- a/src/sqlitecloud/resultset.py
+++ b/src/sqlitecloud/resultset.py
@@ -1,11 +1,13 @@
from typing import Any, Dict, List, Optional
-from sqlitecloud.types import SQCLOUD_RESULT_TYPE
+from sqlitecloud.types import SQLITECLOUD_RESULT_TYPE, SQLITECLOUD_VALUE_TYPE
-class SQCloudResult:
- def __init__(self, tag: SQCLOUD_RESULT_TYPE, result: Optional[any] = None) -> None:
- self.tag: SQCLOUD_RESULT_TYPE = tag
+class SQLiteCloudResult:
+ def __init__(
+ self, tag: SQLITECLOUD_RESULT_TYPE, result: Optional[any] = None
+ ) -> None:
+ self.tag: SQLITECLOUD_RESULT_TYPE = tag
self.nrows: int = 0
self.ncols: int = 0
self.version: int = 0
@@ -31,11 +33,48 @@ def init_data(self, result: any) -> None:
self.data = [result]
self.is_result = True
+ def _compute_index(self, row: int, col: int) -> int:
+ if row < 0 or row >= self.nrows:
+ return -1
+ if col < 0 or col >= self.ncols:
+ return -1
+ return row * self.ncols + col
+
+ def get_value(self, row: int, col: int, convert: bool = True) -> Optional[any]:
+ index = self._compute_index(row, col)
+ if index < 0 or not self.data or index >= len(self.data):
+ return None
+
+ value = self.data[index]
+ return self._convert(value, col) if convert else value
+
+ def get_name(self, col: int) -> Optional[str]:
+ if col < 0 or col >= self.ncols:
+ return None
+ return self.colname[col]
+
+ def _convert(self, value: str, col: int) -> any:
+ if col < 0 or col >= len(self.decltype):
+ return value
+
+ decltype = self.decltype[col]
+ if decltype == SQLITECLOUD_VALUE_TYPE.INTEGER.value:
+ return int(value)
+ if decltype == SQLITECLOUD_VALUE_TYPE.FLOAT.value:
+ return float(value)
+ if decltype == SQLITECLOUD_VALUE_TYPE.BLOB.value:
+ # values are received as bytes before being strings
+ return bytes(value)
+ if decltype == SQLITECLOUD_VALUE_TYPE.NULL.value:
+ return None
+
+ return value
+
-class SqliteCloudResultSet:
- def __init__(self, result: SQCloudResult) -> None:
+class SQLiteCloudResultSet:
+ def __init__(self, result: SQLiteCloudResult) -> None:
self._iter_row: int = 0
- self._result: SQCloudResult = result
+ self._result: SQLiteCloudResult = result
def __getattr__(self, attr: str) -> Optional[Any]:
return getattr(self._result, attr)
@@ -59,23 +98,11 @@ def __next__(self):
raise StopIteration
- def _compute_index(self, row: int, col: int) -> int:
- if row < 0 or row >= self._result.nrows:
- return -1
- if col < 0 or col >= self._result.ncols:
- return -1
- return row * self._result.ncols + col
-
def get_value(self, row: int, col: int) -> Optional[any]:
- index = self._compute_index(row, col)
- if index < 0 or not self._result.data or index >= len(self._result.data):
- return None
- return self._result.data[index]
+ return self._result.get_value(row, col)
def get_name(self, col: int) -> Optional[str]:
- if col < 0 or col >= self._result.ncols:
- return None
- return self._result.colname[col]
+ return self._result.get_name(col)
def get_result(self) -> Optional[any]:
return self.get_value(0, 0)
diff --git a/src/sqlitecloud/types.py b/src/sqlitecloud/types.py
index 242d085..9e29142 100644
--- a/src/sqlitecloud/types.py
+++ b/src/sqlitecloud/types.py
@@ -1,16 +1,20 @@
import types
from asyncio import AbstractEventLoop
from enum import Enum
-from typing import Callable, Optional
+from typing import Any, Callable, Dict, Optional, Union
+from urllib import parse
+# Basic types supported by SQLite Cloud APIs
+SQLiteCloudDataTypes = Union[str, int, bool, Dict[Union[str, int], Any], bytes, None]
-class SQCLOUD_DEFAULT(Enum):
+
+class SQLITECLOUD_DEFAULT(Enum):
PORT = 8860
TIMEOUT = 12
UPLOAD_SIZE = 512 * 1024
-class SQCLOUD_CMD(Enum):
+class SQLITECLOUD_CMD(Enum):
STRING = "+"
ZEROSTRING = "!"
ERROR = "-"
@@ -29,11 +33,19 @@ class SQCLOUD_CMD(Enum):
ARRAY = "="
-class SQCLOUD_ROWSET(Enum):
+class SQLITECLOUD_ROWSET(Enum):
CHUNKS_END = b"/6 0 0 0 "
-class SQCLOUD_INTERNAL_ERRCODE(Enum):
+class SQLITECLOUD_VALUE_TYPE(Enum):
+ INTEGER = "INTEGER"
+ FLOAT = "REAL"
+ TEXT = "TEXT"
+ BLOB = "BLOB"
+ NULL = "NULL"
+
+
+class SQLITECLOUD_INTERNAL_ERRCODE(Enum):
"""
Clients error codes.
"""
@@ -42,7 +54,7 @@ class SQCLOUD_INTERNAL_ERRCODE(Enum):
NETWORK = 100005
-class SQCLOUD_ERRCODE(Enum):
+class SQLITECLOUD_ERRCODE(Enum):
"""
Error codes from Sqlite Cloud.
"""
@@ -56,7 +68,7 @@ class SQCLOUD_ERRCODE(Enum):
RAFT = 10006
-class SQCLOUD_RESULT_TYPE(Enum):
+class SQLITECLOUD_RESULT_TYPE(Enum):
RESULT_OK = 0
RESULT_ERROR = 1
RESULT_STRING = 2
@@ -69,7 +81,7 @@ class SQCLOUD_RESULT_TYPE(Enum):
RESULT_BLOB = 9
-class SQCLOUD_PUBSUB_SUBJECT(Enum):
+class SQLITECLOUD_PUBSUB_SUBJECT(Enum):
"""
Subjects that can be subscribed to by PubSub.
"""
@@ -78,7 +90,7 @@ class SQCLOUD_PUBSUB_SUBJECT(Enum):
CHANNEL = "CHANNEL"
-class SQCloudRowsetSignature:
+class SQLiteCloudRowsetSignature:
"""
Represents the parsed signature for a rowset.
"""
@@ -92,14 +104,14 @@ def __init__(self) -> None:
self.ncols: int = 0
-class SqliteCloudAccount:
+class SQLiteCloudAccount:
def __init__(
self,
username: Optional[str] = "",
password: Optional[str] = "",
hostname: str = "",
dbname: Optional[str] = "",
- port: int = SQCLOUD_DEFAULT.PORT.value,
+ port: int = SQLITECLOUD_DEFAULT.PORT.value,
apikey: Optional[str] = "",
) -> None:
# User name is required unless connectionstring is provided
@@ -117,32 +129,33 @@ def __init__(
self.port = port
-class SQCloudConnect:
+class SQLiteCloudConnect:
"""
Represents the connection information.
"""
def __init__(self):
self.socket: any = None
- self.config: SQCloudConfig
+ self.config: SQLiteCloudConfig
self.isblob: bool = False
self.pubsub_socket: any = None
self.pubsub_callback: Callable[
- [SQCloudConnect, Optional[types.SqliteCloudResultSet], Optional[any]], None
+ [SQLiteCloudConnect, Optional[types.SqliteCloudResultSet], Optional[any]],
+ None,
] = None
self.pubsub_data: any = None
self.pubsub_thread: AbstractEventLoop = None
-class SQCloudConfig:
- def __init__(self) -> None:
- self.account: SqliteCloudAccount = None
+class SQLiteCloudConfig:
+ def __init__(self, connection_str: Optional[str] = None) -> None:
+ self.account: SQLiteCloudAccount = None
# Optional query timeout passed directly to TLS socket
self.timeout = 0
# Socket connection timeout
- self.connect_timeout = SQCLOUD_DEFAULT.TIMEOUT.value
+ self.connect_timeout = SQLITECLOUD_DEFAULT.TIMEOUT.value
# Enable compression
self.compression = False
@@ -173,15 +186,74 @@ def __init__(self) -> None:
# Server should limit total number of rows in a set to maxRowset
self.maxrowset = 0
-
-class SQCloudException(Exception):
+ if connection_str is not None:
+ self._parse_connection_string(connection_str)
+
+ def _parse_connection_string(self, connection_string) -> None:
+ # URL STRING FORMAT
+ # sqlitecloud://user:pass@host.com:port/dbname?timeout=10&key2=value2&key3=value3
+ # or sqlitecloud://host.sqlite.cloud:8860/dbname?apikey=zIiAARzKm9XBVllbAzkB1wqrgijJ3Gx0X5z1A4m4xBA
+
+ self.account = SQLiteCloudAccount()
+
+ try:
+ params = parse.urlparse(connection_string)
+
+ options = {}
+ query = params.query
+ options = parse.parse_qs(query)
+ for option, values in options.items():
+ opt = option.lower()
+ value = values.pop()
+
+ if value.lower() in ["true", "false"]:
+ value = bool(value)
+ elif value.isdigit():
+ value = int(value)
+ else:
+ value = value
+
+ # alias
+ if opt == "nonlinearizable":
+ opt = "non_linearizable"
+
+ if hasattr(self, opt):
+ setattr(self, opt, value)
+ elif hasattr(self.account, opt):
+ setattr(self.account, opt, value)
+
+ # apikey or username/password is accepted
+ if not self.account.apikey:
+ self.account.username = (
+ parse.unquote(params.username) if params.username else ""
+ )
+ self.account.password = (
+ parse.unquote(params.password) if params.password else ""
+ )
+
+ path = params.path
+ database = path.strip("/")
+ if database:
+ self.account.dbname = database
+
+ self.account.hostname = params.hostname
+ self.account.port = (
+ int(params.port) if params.port else SQLITECLOUD_DEFAULT.PORT.value
+ )
+ except Exception as e:
+ raise SQLiteCloudException(
+ f"Invalid connection string {connection_string}"
+ ) from e
+
+
+class SQLiteCloudException(Exception):
def __init__(self, message: str, code: int = -1, xerrcode: int = 0) -> None:
self.errmsg = str(message)
self.errcode = code
self.xerrcode = xerrcode
-class SQCloudNumber:
+class SQLiteCloudNumber:
"""
Represents the parsed number or the error code.
"""
@@ -192,7 +264,7 @@ def __init__(self) -> None:
self.extcode: int = None
-class SQCloudValue:
+class SQLiteCloudValue:
"""
Represents the parse value.
"""
diff --git a/src/sqlitecloud/upload.py b/src/sqlitecloud/upload.py
index bea6578..3dd10e1 100644
--- a/src/sqlitecloud/upload.py
+++ b/src/sqlitecloud/upload.py
@@ -4,7 +4,7 @@
from typing import Optional
from sqlitecloud.driver import Driver
-from sqlitecloud.types import SQCloudConnect
+from sqlitecloud.types import SQLiteCloudConnect
def xCallback(fd: BufferedReader, blen: int, ntot: int, nprogress: int) -> bytes:
@@ -32,19 +32,19 @@ def xCallback(fd: BufferedReader, blen: int, ntot: int, nprogress: int) -> bytes
def upload_db(
- connection: SQCloudConnect, dbname: str, key: Optional[str], filename: str
+ connection: SQLiteCloudConnect, dbname: str, key: Optional[str], filename: str
) -> None:
"""
Uploads a SQLite database to the SQLite Cloud node using the provided connection.
Args:
- connection (SQCloudConnect): The connection object used to connect to the node.
+ connection (SQLiteCloudConnect): The connection object used to connect to the node.
dbname (str): The name of the database in SQLite Cloud.
key (Optional[str]): The encryption key for the database. If None, no encryption is used.
filename (str): The path to the SQLite database file to be uploaded.
Raises:
- SQCloudException: If an error occurs while uploading the database.
+ SQLiteCloudException: If an error occurs while uploading the database.
"""
diff --git a/src/tests/assets/chinook.sqlite b/src/tests/assets/chinook.sqlite
new file mode 100644
index 0000000..60621b9
Binary files /dev/null and b/src/tests/assets/chinook.sqlite differ
diff --git a/src/tests/assets/prices.csv b/src/tests/assets/prices.csv
new file mode 100644
index 0000000..4c822e5
--- /dev/null
+++ b/src/tests/assets/prices.csv
@@ -0,0 +1,19 @@
+DATE,CURRENCY,PRICE
+20230504,USD,201.23456
+20230503,USD,12.34567
+20230502,USD,23.45678
+20230501,USD,34.56789
+20230430,USD,45.6789
+20230425,USD,90.12345
+20230424,USD,1.23456
+20230423,USD,2.34567
+20230422,USD,3.45678
+20230421,USD,4.56789
+20230416,USD,9.01234
+20230415,USD,0.12345
+20230414,USD,0.23456
+20230413,USD,0.34567
+20230412,USD,0.45678
+20230411,USD,0.56789
+20230410,USD,0.67890
+20230409,USD,0.78901
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index da4f982..b1db511 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -3,8 +3,9 @@
import pytest
from dotenv import load_dotenv
-from sqlitecloud.client import SqliteCloudClient
-from sqlitecloud.types import SQCloudConnect, SqliteCloudAccount
+import sqlitecloud
+from sqlitecloud.client import SQLiteCloudClient
+from sqlitecloud.types import SQLiteCloudAccount, SQLiteCloudConnect
@pytest.fixture(autouse=True)
@@ -14,19 +15,37 @@ def load_env_vars():
@pytest.fixture()
def sqlitecloud_connection():
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.username = os.getenv("SQLITE_USER")
account.password = os.getenv("SQLITE_PASSWORD")
account.dbname = os.getenv("SQLITE_DB")
account.hostname = os.getenv("SQLITE_HOST")
account.port = int(os.getenv("SQLITE_PORT"))
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
connection = client.open_connection()
- assert isinstance(connection, SQCloudConnect)
+ assert isinstance(connection, SQLiteCloudConnect)
assert client.is_connected(connection)
yield (connection, client)
client.disconnect(connection)
+
+
+@pytest.fixture()
+def sqlitecloud_dbapi2_connection():
+ account = SQLiteCloudAccount()
+ account.username = os.getenv("SQLITE_USER")
+ account.password = os.getenv("SQLITE_PASSWORD")
+ account.dbname = os.getenv("SQLITE_DB")
+ account.hostname = os.getenv("SQLITE_HOST")
+ account.port = int(os.getenv("SQLITE_PORT"))
+
+ connection = sqlitecloud.connect(account)
+
+ assert isinstance(connection, sqlitecloud.Connection)
+
+ yield connection
+
+ connection.close()
diff --git a/src/tests/integration/test_client.py b/src/tests/integration/test_client.py
index 273bb19..f0a7a81 100644
--- a/src/tests/integration/test_client.py
+++ b/src/tests/integration/test_client.py
@@ -4,14 +4,14 @@
import pytest
-from sqlitecloud.client import SqliteCloudClient
+from sqlitecloud.client import SQLiteCloudClient
from sqlitecloud.types import (
- SQCLOUD_ERRCODE,
- SQCLOUD_INTERNAL_ERRCODE,
- SQCLOUD_RESULT_TYPE,
- SQCloudConnect,
- SQCloudException,
- SqliteCloudAccount,
+ SQLITECLOUD_ERRCODE,
+ SQLITECLOUD_INTERNAL_ERRCODE,
+ SQLITECLOUD_RESULT_TYPE,
+ SQLiteCloudAccount,
+ SQLiteCloudConnect,
+ SQLiteCloudException,
)
@@ -23,69 +23,69 @@ class TestClient:
EXPECT_SPEED_MS = 6 * 1000
def test_connection_with_credentials(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.username = os.getenv("SQLITE_USER")
account.password = os.getenv("SQLITE_PASSWORD")
account.dbname = os.getenv("SQLITE_DB")
account.hostname = os.getenv("SQLITE_HOST")
account.port = int(os.getenv("SQLITE_PORT"))
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
conn = client.open_connection()
- assert isinstance(conn, SQCloudConnect)
+ assert isinstance(conn, SQLiteCloudConnect)
client.disconnect(conn)
def test_connection_with_apikey(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.username = os.getenv("SQLITE_API_KEY")
account.hostname = os.getenv("SQLITE_HOST")
account.port = int(os.getenv("SQLITE_PORT"))
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
conn = client.open_connection()
- assert isinstance(conn, SQCloudConnect)
+ assert isinstance(conn, SQLiteCloudConnect)
client.disconnect(conn)
def test_connection_without_credentials_and_apikey(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.dbname = os.getenv("SQLITE_DB")
account.hostname = os.getenv("SQLITE_HOST")
account.port = int(os.getenv("SQLITE_PORT"))
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
- with pytest.raises(SQCloudException):
+ with pytest.raises(SQLiteCloudException):
client.open_connection()
def test_connect_with_string(self):
connection_string = os.getenv("SQLITE_CONNECTION_STRING")
- client = SqliteCloudClient(connection_str=connection_string)
+ client = SQLiteCloudClient(connection_str=connection_string)
conn = client.open_connection()
- assert isinstance(conn, SQCloudConnect)
+ assert isinstance(conn, SQLiteCloudConnect)
client.disconnect(conn)
def test_connect_with_string_with_credentials(self):
connection_string = f"sqlitecloud://{os.getenv('SQLITE_USER')}:{os.getenv('SQLITE_PASSWORD')}@{os.getenv('SQLITE_HOST')}/{os.getenv('SQLITE_DB')}"
- client = SqliteCloudClient(connection_str=connection_string)
+ client = SQLiteCloudClient(connection_str=connection_string)
conn = client.open_connection()
- assert isinstance(conn, SQCloudConnect)
+ assert isinstance(conn, SQLiteCloudConnect)
client.disconnect(conn)
def test_is_connected(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.username = os.getenv("SQLITE_API_KEY")
account.hostname = os.getenv("SQLITE_HOST")
account.port = int(os.getenv("SQLITE_PORT"))
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
conn = client.open_connection()
assert client.is_connected(conn)
@@ -94,12 +94,12 @@ def test_is_connected(self):
assert not client.is_connected(conn)
def test_disconnect(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.username = os.getenv("SQLITE_API_KEY")
account.hostname = os.getenv("SQLITE_HOST")
account.port = int(os.getenv("SQLITE_PORT"))
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
conn = client.open_connection()
assert client.is_connected(conn)
@@ -124,7 +124,7 @@ def test_select(self, sqlitecloud_connection):
def test_column_not_found(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
- with pytest.raises(SQCloudException) as e:
+ with pytest.raises(SQLiteCloudException) as e:
client.exec_query("SELECT not_a_column FROM albums", connection)
assert e.value.errcode == 1
@@ -134,7 +134,7 @@ def test_rowset_data(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("SELECT AlbumId FROM albums LIMIT 2", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_ROWSET == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET == result.tag
assert 2 == result.nrows
assert 1 == result.ncols
assert 2 == result.version
@@ -143,9 +143,9 @@ def test_get_value(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("SELECT * FROM albums", connection)
- assert "1" == result.get_value(0, 0)
+ assert 1 == result.get_value(0, 0)
assert "For Those About To Rock We Salute You" == result.get_value(0, 1)
- assert "2" == result.get_value(1, 0)
+ assert 2 == result.get_value(1, 0)
def test_select_utf8_value_and_column_name(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
@@ -194,28 +194,28 @@ def test_integer(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST INTEGER", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_INTEGER == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_INTEGER == result.tag
assert 123456 == result.get_result()
def test_float(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST FLOAT", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_FLOAT == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_FLOAT == result.tag
assert 3.1415926 == result.get_result()
def test_string(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST STRING", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_STRING == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_STRING == result.tag
assert result.get_result() == "Hello World, this is a test string."
def test_zero_string(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST ZERO_STRING", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_STRING == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_STRING == result.tag
assert (
result.get_result() == "Hello World, this is a zero-terminated test string."
)
@@ -224,7 +224,7 @@ def test_empty_string(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST STRING0", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_STRING == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_STRING == result.tag
assert result.get_result() == ""
def test_command(self, sqlitecloud_connection):
@@ -237,7 +237,7 @@ def test_json(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST JSON", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_JSON == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_JSON == result.tag
assert {
"msg-from": {"class": "soldier", "name": "Wixilav"},
"msg-to": {"class": "supreme-commander", "name": "[Redacted]"},
@@ -254,20 +254,20 @@ def test_blob(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST BLOB", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_BLOB == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_BLOB == result.tag
assert len(result.get_result()) == 1000
def test_blob0(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST BLOB0", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_STRING == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_STRING == result.tag
assert len(result.get_result()) == 0
def test_error(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
- with pytest.raises(SQCloudException) as e:
+ with pytest.raises(SQLiteCloudException) as e:
client.exec_query("TEST ERROR", connection)
assert e.value.errcode == 66666
@@ -276,7 +276,7 @@ def test_error(self, sqlitecloud_connection):
def test_ext_error(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
- with pytest.raises(SQCloudException) as e:
+ with pytest.raises(SQLiteCloudException) as e:
client.exec_query("TEST EXTERROR", connection)
assert e.value.errcode == 66666
@@ -292,7 +292,7 @@ def test_array(self, sqlitecloud_connection):
result_array = result.get_result()
- assert SQCLOUD_RESULT_TYPE.RESULT_ARRAY == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_ARRAY == result.tag
assert isinstance(result_array, list)
assert len(result_array) == 5
assert result_array[0] == "Hello World"
@@ -304,7 +304,7 @@ def test_rowset(self, sqlitecloud_connection):
connection, client = sqlitecloud_connection
result = client.exec_query("TEST ROWSET", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_ROWSET == result.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET == result.tag
assert result.nrows >= 30
assert result.ncols == 2
assert result.version in [1, 2]
@@ -312,12 +312,12 @@ def test_rowset(self, sqlitecloud_connection):
assert result.get_name(1) == "value"
def test_max_rows_option(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.hostname = os.getenv("SQLITE_HOST")
account.dbname = os.getenv("SQLITE_DB")
account.apikey = os.getenv("SQLITE_API_KEY")
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
client.config.maxrows = 1
connection = client.open_connection()
@@ -331,31 +331,31 @@ def test_max_rows_option(self):
assert rowset.nrows > 100
def test_max_rowset_option_to_fail_when_rowset_is_bigger(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.hostname = os.getenv("SQLITE_HOST")
account.dbname = os.getenv("SQLITE_DB")
account.apikey = os.getenv("SQLITE_API_KEY")
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
client.config.maxrowset = 1024
connection = client.open_connection()
- with pytest.raises(SQCloudException) as e:
+ with pytest.raises(SQLiteCloudException) as e:
client.exec_query("SELECT * FROM albums", connection)
client.disconnect(connection)
- assert SQCLOUD_ERRCODE.INTERNAL.value == e.value.errcode
+ assert SQLITECLOUD_ERRCODE.INTERNAL.value == e.value.errcode
assert "RowSet too big to be sent (limit set to 1024 bytes)." == e.value.errmsg
def test_max_rowset_option_to_succeed_when_rowset_is_lighter(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.hostname = os.getenv("SQLITE_HOST")
account.dbname = os.getenv("SQLITE_DB")
account.apikey = os.getenv("SQLITE_API_KEY")
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
client.config.maxrowset = 1024
connection = client.open_connection()
@@ -371,7 +371,7 @@ def test_chunked_rowset(self, sqlitecloud_connection):
rowset = client.exec_query("TEST ROWSET_CHUNK", connection)
- assert SQCLOUD_RESULT_TYPE.RESULT_ROWSET == rowset.tag
+ assert SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET == rowset.tag
assert 147 == rowset.nrows
assert 1 == rowset.ncols
assert 147 == len(rowset.data)
@@ -413,18 +413,18 @@ def test_serialized_operations(self, sqlitecloud_connection):
assert rowset.version in [1, 2]
def test_query_timeout(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.hostname = os.getenv("SQLITE_HOST")
account.dbname = os.getenv("SQLITE_DB")
account.apikey = os.getenv("SQLITE_API_KEY")
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
client.config.timeout = 1 # 1 sec
connection = client.open_connection()
# this operation should take more than 1 sec
- with pytest.raises(SQCloudException) as e:
+ with pytest.raises(SQLiteCloudException) as e:
# just a long running query
client.exec_query(
"""
@@ -440,7 +440,7 @@ def test_query_timeout(self):
client.disconnect(connection)
- assert e.value.errcode == SQCLOUD_INTERNAL_ERRCODE.NETWORK
+ assert e.value.errcode == SQLITECLOUD_INTERNAL_ERRCODE.NETWORK
assert e.value.errmsg == "An error occurred while reading data from the socket."
def test_XXL_query(self, sqlitecloud_connection):
@@ -508,12 +508,12 @@ def test_select_long_formatted_string(self, sqlitecloud_connection):
assert len(rowset.get_value(0, 0)) == 1000
def test_select_database(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.hostname = os.getenv("SQLITE_HOST")
account.dbname = ""
account.apikey = os.getenv("SQLITE_API_KEY")
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
connection = client.open_connection()
@@ -604,12 +604,12 @@ def test_stress_test_20x_batched_selects(self, sqlitecloud_connection):
), f"{num_queries}x batched selects, {query_ms}ms per query"
def test_compression_single_column(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.hostname = os.getenv("SQLITE_HOST")
account.apikey = os.getenv("SQLITE_API_KEY")
account.dbname = os.getenv("SQLITE_DB")
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
client.config.compression = True
connection = client.open_connection()
@@ -629,12 +629,12 @@ def test_compression_single_column(self):
assert len(rowset.get_value(0, 0)) == blob_size * 2
def test_compression_multiple_columns(self):
- account = SqliteCloudAccount()
+ account = SQLiteCloudAccount()
account.hostname = os.getenv("SQLITE_HOST")
account.apikey = os.getenv("SQLITE_API_KEY")
account.dbname = os.getenv("SQLITE_DB")
- client = SqliteCloudClient(cloud_account=account)
+ client = SQLiteCloudClient(cloud_account=account)
client.config.compression = True
connection = client.open_connection()
@@ -650,3 +650,30 @@ def test_compression_multiple_columns(self):
assert rowset.nrows > 0
assert rowset.ncols > 0
assert rowset.get_name(0) == "AlbumId"
+
+ def test_exec_statement_with_named_placeholder(self, sqlitecloud_connection):
+ connection, client = sqlitecloud_connection
+
+ result = client.exec_statement(
+ "SELECT * FROM albums WHERE AlbumId = :id and Title = :title",
+ {"id": 1, "title": "For Those About To Rock We Salute You"},
+ connection,
+ )
+
+ assert result.nrows == 1
+ assert result.get_value(0, 0) == 1
+
+ def test_exec_statement_with_qmarks(self, sqlitecloud_connection):
+ connection, client = sqlitecloud_connection
+
+ result = client.exec_statement(
+ "SELECT * FROM albums WHERE AlbumId = ? and Title = ?",
+ (
+ 1,
+ "For Those About To Rock We Salute You",
+ ),
+ connection,
+ )
+
+ assert result.nrows == 1
+ assert result.get_value(0, 0) == 1
diff --git a/src/tests/integration/test_dbapi2.py b/src/tests/integration/test_dbapi2.py
new file mode 100644
index 0000000..e97f383
--- /dev/null
+++ b/src/tests/integration/test_dbapi2.py
@@ -0,0 +1,248 @@
+import os
+import uuid
+
+import pytest
+
+import sqlitecloud
+from sqlitecloud.types import (
+ SQLITECLOUD_INTERNAL_ERRCODE,
+ SQLiteCloudAccount,
+ SQLiteCloudException,
+)
+
+
+class TestDBAPI2:
+ def test_connect_with_account(self):
+ account = SQLiteCloudAccount(
+ os.getenv("SQLITE_USER"),
+ os.getenv("SQLITE_PASSWORD"),
+ os.getenv("SQLITE_HOST"),
+ os.getenv("SQLITE_DB"),
+ int(os.getenv("SQLITE_PORT")),
+ )
+
+ connection = sqlitecloud.connect(account)
+
+ connection.close()
+ assert isinstance(connection, sqlitecloud.Connection)
+
+ def test_connect_with_connection_string(self):
+ connection_str = f"{os.getenv('SQLITE_CONNECTION_STRING')}/{os.getenv('SQLITE_DB')}?apikey={os.getenv('SQLITE_API_KEY')}"
+
+ connection = sqlitecloud.connect(connection_str)
+
+ connection.close()
+ assert isinstance(connection, sqlitecloud.Connection)
+
+ def test_disconnect(self):
+ account = SQLiteCloudAccount(
+ os.getenv("SQLITE_USER"),
+ os.getenv("SQLITE_PASSWORD"),
+ os.getenv("SQLITE_HOST"),
+ os.getenv("SQLITE_DB"),
+ int(os.getenv("SQLITE_PORT")),
+ )
+
+ connection = sqlitecloud.connect(account)
+
+ connection.close()
+
+ assert isinstance(connection, sqlitecloud.Connection)
+
+ with pytest.raises(SQLiteCloudException) as e:
+ connection.execute("SELECT 1")
+
+ assert e.value.errcode == SQLITECLOUD_INTERNAL_ERRCODE.NETWORK
+ assert e.value.errmsg == "The connection is closed."
+
+ def test_select(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ cursor = connection.cursor()
+ cursor.execute("SELECT 'Hello'")
+
+ result = cursor.fetchone()
+
+ assert result == ("Hello",)
+
+ def test_connection_execute(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ cursor = connection.execute("SELECT 'Hello'")
+
+ result = cursor.fetchone()
+
+ assert result == ("Hello",)
+
+ def test_column_not_found(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ with pytest.raises(SQLiteCloudException) as e:
+ connection.execute("SELECT not_a_column FROM albums")
+
+ assert e.value.errcode == 1
+ assert e.value.errmsg == "no such column: not_a_column"
+
+ def test_rowset_data(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+ cursor = connection.execute("SELECT AlbumId FROM albums LIMIT 2")
+
+ assert cursor.rowcount == 2
+ assert len(cursor.description) == 1
+
+ def test_fetch_one_row(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ cursor = connection.execute("SELECT * FROM albums")
+
+ row = cursor.fetchone()
+
+ assert len(row) == 3
+ assert row == (1, "For Those About To Rock We Salute You", 1)
+
+ def test_select_utf8_value_and_column_name(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+ cursor = connection.execute("SELECT 'Minha História'")
+
+ assert cursor.rowcount == 1
+ assert len(cursor.description) == 1
+ assert "Minha História" == cursor.fetchone()[0]
+ assert "'Minha História'" == cursor.description[0][0]
+
+ def test_column_name(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+ cursor = connection.execute("SELECT * FROM albums")
+
+ assert "AlbumId" == cursor.description[0][0]
+ assert "Title" == cursor.description[1][0]
+
+ def test_integer(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+ cursor = connection.execute("TEST INTEGER")
+
+ assert cursor.rowcount == -1
+ assert cursor.fetchone() is None
+
+ def test_error(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ with pytest.raises(SQLiteCloudException) as e:
+ connection.execute("TEST ERROR")
+
+ assert e.value.errcode == 66666
+ assert e.value.errmsg == "This is a test error message with a devil error code."
+
+ def test_execute_with_named_placeholder(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ cursor = connection.execute(
+ "SELECT * FROM albums WHERE AlbumId = :id and Title like :title",
+ {"id": 1, "title": "For Those About%"},
+ )
+
+ assert cursor.rowcount == 1
+ assert cursor.fetchone() == (1, "For Those About To Rock We Salute You", 1)
+
+ def test_execute_with_qmarks(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ cursor = connection.execute(
+ "SELECT * FROM albums WHERE AlbumId = ? and Title like ?",
+ (
+ 1,
+ "For Those About%",
+ ),
+ )
+
+ assert cursor.rowcount == 1
+ assert cursor.fetchone() == (1, "For Those About To Rock We Salute You", 1)
+
+ def test_execute_two_updates_using_the_cursor(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ cursor = connection.cursor()
+ new_name1 = "Jazz" + str(uuid.uuid4())
+ new_name2 = "Jazz" + str(uuid.uuid4())
+ genreId = 2
+
+ cursor.execute(
+ "UPDATE genres SET Name = ? WHERE GenreId = ?;",
+ (
+ new_name1,
+ genreId,
+ ),
+ )
+ cursor.execute(
+ "UPDATE genres SET Name = ? WHERE GenreId = ?;",
+ (
+ new_name2,
+ genreId,
+ ),
+ )
+
+ cursor.execute(
+ "SELECT Name, GenreID FROM genres WHERE GenreId = :id", {"id": genreId}
+ )
+
+ assert cursor.fetchone() == (
+ new_name2,
+ genreId,
+ )
+
+ def test_executemany_updates(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ new_name1 = "Jazz" + str(uuid.uuid4())
+ new_name2 = "Jazz" + str(uuid.uuid4())
+ genreId = 2
+
+ cursor = connection.executemany(
+ "UPDATE genres SET Name = ? WHERE GenreId = ?;",
+ [(new_name1, genreId), (new_name2, genreId)],
+ )
+
+ cursor.execute(
+ "SELECT Name, GenreID FROM genres WHERE GenreId = :id", {"id": genreId}
+ )
+
+ assert cursor.fetchone() == (
+ new_name2,
+ genreId,
+ )
+
+ def test_executemany_updates_using_the_cursor(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ cursor = connection.cursor()
+ new_name1 = "Jazz" + str(uuid.uuid4())
+ new_name2 = "Jazz" + str(uuid.uuid4())
+ genreId = 2
+
+ cursor.executemany(
+ "UPDATE genres SET Name = ? WHERE GenreId = ?;",
+ [(new_name1, genreId), (new_name2, genreId)],
+ )
+
+ cursor.execute(
+ "SELECT Name, GenreID FROM genres WHERE GenreId = :id", {"id": genreId}
+ )
+
+ assert cursor.fetchone() == (
+ new_name2,
+ genreId,
+ )
+
+ def test_row_factory(self, sqlitecloud_dbapi2_connection):
+ connection = sqlitecloud_dbapi2_connection
+
+ connection.row_factory = lambda cursor, row: {
+ description[0]: row[i] for i, description in enumerate(cursor.description)
+ }
+
+ cursor = connection.execute("SELECT * FROM albums")
+
+ row = cursor.fetchone()
+
+ assert row["AlbumId"] == 1
+ assert row["Title"] == "For Those About To Rock We Salute You"
+ assert row["ArtistId"] == 1
diff --git a/src/tests/integration/test_download.py b/src/tests/integration/test_download.py
index e40825c..ddd5f46 100644
--- a/src/tests/integration/test_download.py
+++ b/src/tests/integration/test_download.py
@@ -4,7 +4,7 @@
import pytest
from sqlitecloud import download
-from sqlitecloud.types import SQCLOUD_ERRCODE, SQCloudException
+from sqlitecloud.types import SQLITECLOUD_ERRCODE, SQLiteCloudException
class TestDownload:
@@ -25,8 +25,8 @@ def test_download_missing_database(self, sqlitecloud_connection):
temp_file = tempfile.mkstemp(prefix="missing")[1]
- with pytest.raises(SQCloudException) as e:
+ with pytest.raises(SQLiteCloudException) as e:
download.download_db(connection, "missing.sqlite", temp_file)
- assert e.value.errcode == SQCLOUD_ERRCODE.COMMAND.value
+ assert e.value.errcode == SQLITECLOUD_ERRCODE.COMMAND.value
assert e.value.errmsg == "Database missing.sqlite does not exist."
diff --git a/src/tests/integration/test_pandas.py b/src/tests/integration/test_pandas.py
new file mode 100644
index 0000000..799627b
--- /dev/null
+++ b/src/tests/integration/test_pandas.py
@@ -0,0 +1,65 @@
+import os
+
+import pandas as pd
+from pandas.testing import assert_frame_equal
+
+
+# Integration tests for sqlitecloud and pandas dataframe
+class TestPanads:
+ def test_insert_from_dataframe(self, sqlitecloud_dbapi2_connection):
+ conn = sqlitecloud_dbapi2_connection
+
+ dfprices = pd.read_csv(
+ os.path.join(os.path.dirname(__file__), "../assets/prices.csv")
+ )
+
+ dfmapping = pd.DataFrame(
+ {
+ "AXP": ["American Express Company"],
+ "GE": ["General Electric Company"],
+ "GS": ["Goldman Sachs Group Inc"],
+ "UTX": ["United Technologies Corporation"],
+ }
+ )
+
+ conn.executemany("DROP TABLE IF EXISTS ?", [("PRICES",), ("TICKER_MAPPING",)])
+
+ # arg if_exists="replace" raises the error
+ dfprices.to_sql("PRICES", conn, index=False)
+ dfmapping.to_sql("TICKER_MAPPING", conn, index=False)
+
+ df_actual_tables = pd.read_sql(
+ "SELECT name FROM sqlite_master WHERE type='table'", conn
+ )
+ df_actual_prices = pd.read_sql("SELECT * FROM PRICES", conn)
+ df_actual_mapping = pd.read_sql("SELECT * FROM TICKER_MAPPING", conn)
+
+ assert "PRICES" in df_actual_tables["name"].to_list()
+ assert "TICKER_MAPPING" in df_actual_tables["name"].to_list()
+ assert_frame_equal(
+ df_actual_prices,
+ dfprices,
+ check_exact=False,
+ atol=1e-6,
+ check_dtype=False,
+ )
+ assert_frame_equal(
+ df_actual_mapping,
+ dfmapping,
+ check_exact=False,
+ atol=1e-6,
+ check_dtype=False,
+ )
+
+ def test_select_into_dataframe(self, sqlitecloud_dbapi2_connection):
+ conn = sqlitecloud_dbapi2_connection
+
+ query = "SELECT * FROM albums"
+ df = pd.read_sql_query(query, conn)
+ cursor = conn.execute(query)
+
+ assert df.columns.to_list() == [
+ description[0] for description in cursor.description
+ ]
+ # compare as tuples
+ assert list(df.itertuples(index=False, name=None)) == cursor.fetchall()
diff --git a/src/tests/integration/test_pubsub.py b/src/tests/integration/test_pubsub.py
index 2b037ba..be4b98a 100644
--- a/src/tests/integration/test_pubsub.py
+++ b/src/tests/integration/test_pubsub.py
@@ -3,13 +3,13 @@
import pytest
-from sqlitecloud.pubsub import SqliteCloudPubSub
-from sqlitecloud.resultset import SqliteCloudResultSet
+from sqlitecloud.pubsub import SQLiteCloudPubSub
+from sqlitecloud.resultset import SQLiteCloudResultSet
from sqlitecloud.types import (
- SQCLOUD_ERRCODE,
- SQCLOUD_PUBSUB_SUBJECT,
- SQCLOUD_RESULT_TYPE,
- SQCloudException,
+ SQLITECLOUD_ERRCODE,
+ SQLITECLOUD_PUBSUB_SUBJECT,
+ SQLITECLOUD_RESULT_TYPE,
+ SQLiteCloudException,
)
@@ -24,14 +24,14 @@ def assert_callback(conn, result, data):
nonlocal callback_called
nonlocal flag
- if isinstance(result, SqliteCloudResultSet):
- assert result.tag == SQCLOUD_RESULT_TYPE.RESULT_JSON
+ if isinstance(result, SQLiteCloudResultSet):
+ assert result.tag == SQLITECLOUD_RESULT_TYPE.RESULT_JSON
assert data == ["somedata"]
callback_called = True
flag.set()
- pubsub = SqliteCloudPubSub()
- type = SQCLOUD_PUBSUB_SUBJECT.CHANNEL
+ pubsub = SQLiteCloudPubSub()
+ type = SQLITECLOUD_PUBSUB_SUBJECT.CHANNEL
channel = "channel" + str(uuid.uuid4())
pubsub.create_channel(connection, channel)
@@ -47,8 +47,8 @@ def assert_callback(conn, result, data):
def test_unlisten_channel(self, sqlitecloud_connection):
connection, _ = sqlitecloud_connection
- pubsub = SqliteCloudPubSub()
- type = SQCLOUD_PUBSUB_SUBJECT.CHANNEL
+ pubsub = SQLiteCloudPubSub()
+ type = SQLITECLOUD_PUBSUB_SUBJECT.CHANNEL
channel_name = "channel" + str(uuid.uuid4())
pubsub.create_channel(connection, channel_name)
@@ -68,24 +68,24 @@ def test_unlisten_channel(self, sqlitecloud_connection):
def test_create_channel_to_fail_if_exists(self, sqlitecloud_connection):
connection, _ = sqlitecloud_connection
- pubsub = SqliteCloudPubSub()
+ pubsub = SQLiteCloudPubSub()
channel_name = "channel" + str(uuid.uuid4())
pubsub.create_channel(connection, channel_name, if_not_exists=True)
- with pytest.raises(SQCloudException) as e:
+ with pytest.raises(SQLiteCloudException) as e:
pubsub.create_channel(connection, channel_name, if_not_exists=False)
assert (
e.value.errmsg
== f"Cannot create channel {channel_name} because it already exists."
)
- assert e.value.errcode == SQCLOUD_ERRCODE.GENERIC.value
+ assert e.value.errcode == SQLITECLOUD_ERRCODE.GENERIC.value
def test_is_connected(self, sqlitecloud_connection):
connection, _ = sqlitecloud_connection
- pubsub = SqliteCloudPubSub()
+ pubsub = SQLiteCloudPubSub()
channel_name = "channel" + str(uuid.uuid4())
assert not pubsub.is_connected(connection)
@@ -93,7 +93,7 @@ def test_is_connected(self, sqlitecloud_connection):
pubsub.create_channel(connection, channel_name, if_not_exists=True)
pubsub.listen(
connection,
- SQCLOUD_PUBSUB_SUBJECT.CHANNEL,
+ SQLITECLOUD_PUBSUB_SUBJECT.CHANNEL,
channel_name,
lambda conn, result, data: None,
)
@@ -110,13 +110,13 @@ def assert_callback(conn, result, data):
nonlocal callback_called
nonlocal flag
- if isinstance(result, SqliteCloudResultSet):
+ if isinstance(result, SQLiteCloudResultSet):
assert result.get_result() is not None
callback_called = True
flag.set()
- pubsub = SqliteCloudPubSub()
- type = SQCLOUD_PUBSUB_SUBJECT.CHANNEL
+ pubsub = SQLiteCloudPubSub()
+ type = SQLITECLOUD_PUBSUB_SUBJECT.CHANNEL
channel = "channel" + str(uuid.uuid4())
pubsub.create_channel(connection, channel, if_not_exists=True)
@@ -128,7 +128,7 @@ def assert_callback(conn, result, data):
assert pubsub.is_connected(connection)
connection2 = client.open_connection()
- pubsub2 = SqliteCloudPubSub()
+ pubsub2 = SQLiteCloudPubSub()
pubsub2.notify_channel(connection2, channel, "message-in-a-bottle")
client.disconnect(connection2)
@@ -148,15 +148,15 @@ def assert_callback(conn, result, data):
nonlocal callback_called
nonlocal flag
- if isinstance(result, SqliteCloudResultSet):
- assert result.tag == SQCLOUD_RESULT_TYPE.RESULT_JSON
+ if isinstance(result, SQLiteCloudResultSet):
+ assert result.tag == SQLITECLOUD_RESULT_TYPE.RESULT_JSON
assert new_name in result.get_result()
assert data == ["somedata"]
callback_called = True
flag.set()
- pubsub = SqliteCloudPubSub()
- type = SQCLOUD_PUBSUB_SUBJECT.TABLE
+ pubsub = SQLiteCloudPubSub()
+ type = SQLITECLOUD_PUBSUB_SUBJECT.TABLE
new_name = "Rock" + str(uuid.uuid4())
pubsub.listen(connection, type, "genres", assert_callback, ["somedata"])
diff --git a/src/tests/integration/test_sqlite3_parity.py b/src/tests/integration/test_sqlite3_parity.py
new file mode 100644
index 0000000..e80fc52
--- /dev/null
+++ b/src/tests/integration/test_sqlite3_parity.py
@@ -0,0 +1,245 @@
+import os
+import sqlite3
+
+import pytest
+
+from sqlitecloud.types import SQLiteCloudException
+
+
+class TestSQLite3FeatureParity:
+ @pytest.fixture()
+ def sqlite3_connection(self):
+ connection = sqlite3.connect(
+ os.path.join(os.path.dirname(__file__), "../assets/chinook.sqlite")
+ )
+ yield connection
+ connection.close()
+
+ def test_connection_close(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ sqlitecloud_connection.close()
+ sqlite3_connection.close()
+
+ with pytest.raises(SQLiteCloudException) as e:
+ sqlitecloud_connection.execute("SELECT 1")
+
+ assert isinstance(e.value, SQLiteCloudException)
+
+ with pytest.raises(sqlite3.ProgrammingError) as e:
+ sqlite3_connection.execute("SELECT 1")
+
+ assert isinstance(e.value, sqlite3.ProgrammingError)
+
+ @pytest.mark.skip(
+ reason="SQLite Cloud does not convert to int a column without an explicit SQLite Type"
+ )
+ def test_ping_select(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ sqlitecloud_cursor = sqlitecloud_connection.execute("SELECT 1")
+ sqlite3_cursor = sqlite3_connection.execute("SELECT 1")
+
+ sqlitecloud_cursor = sqlitecloud_cursor.fetchall()
+ sqlite3_cursor = sqlite3_cursor.fetchall()
+
+ assert sqlitecloud_cursor == sqlite3_cursor
+
+ def test_create_table_and_insert_many(
+ self, sqlitecloud_dbapi2_connection, sqlite3_connection
+ ):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ create_table_query = "CREATE TABLE IF NOT EXISTS sqlitetest (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"
+ sqlitecloud_connection.execute(create_table_query)
+ sqlite3_connection.execute(create_table_query)
+
+ truncate_table_query = "DELETE FROM sqlitetest"
+ sqlitecloud_connection.execute(truncate_table_query)
+ sqlite3_connection.execute(truncate_table_query)
+
+ insert_query = "INSERT INTO sqlitetest (name, age) VALUES (?, ?)"
+ params = [("Alice", 25), ("Bob", 30)]
+ sqlitecloud_connection.executemany(insert_query, params)
+ sqlite3_connection.executemany(insert_query, params)
+
+ select_query = "SELECT * FROM sqlitetest"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchall()
+ sqlite3_results = sqlite3_cursor.fetchall()
+
+ assert sqlitecloud_results == sqlite3_results
+
+ def test_execute_with_question_mark_style(
+ self, sqlitecloud_dbapi2_connection, sqlite3_connection
+ ):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT * FROM albums WHERE AlbumId = ?"
+ params = (1,)
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query, params)
+ sqlite3_cursor = sqlite3_connection.execute(select_query, params)
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchall()
+ sqlite3_results = sqlite3_cursor.fetchall()
+
+ assert sqlitecloud_results == sqlite3_results
+
+ def test_execute_with_named_param_style(
+ self, sqlitecloud_dbapi2_connection, sqlite3_connection
+ ):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT * FROM albums WHERE AlbumId = :id"
+ params = {"id": 1}
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query, params)
+ sqlite3_cursor = sqlite3_connection.execute(select_query, params)
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchall()
+ sqlite3_results = sqlite3_cursor.fetchall()
+
+ assert sqlitecloud_results == sqlite3_results
+
+ @pytest.mark.skip(
+ reason="Rowcount does not contain the number of inserted rows yet"
+ )
+ def test_insert_result(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ insert_query = "INSERT INTO albums (Title, ArtistId) VALUES (?, ?)"
+ params = ("Test Album", 1)
+ sqlitecloud_cursor = sqlitecloud_connection.execute(insert_query, params)
+ sqlite3_cursor = sqlite3_connection.execute(insert_query, params)
+
+ assert sqlitecloud_cursor.rowcount == sqlite3_cursor.rowcount
+
+ def test_close_cursor_raises_exception(
+ self, sqlitecloud_dbapi2_connection, sqlite3_connection
+ ):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT 1"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ sqlitecloud_cursor.close()
+ sqlite3_cursor.close()
+
+ with pytest.raises(SQLiteCloudException) as e:
+ sqlitecloud_cursor.fetchall()
+
+ assert isinstance(e.value, SQLiteCloudException)
+
+ with pytest.raises(sqlite3.ProgrammingError) as e:
+ sqlite3_cursor.fetchall()
+
+ def test_row_factory(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ def simple_factory(cursor, row):
+ return {
+ description[0]: row[i]
+ for i, description in enumerate(cursor.description)
+ }
+
+ sqlitecloud_connection.row_factory = simple_factory
+ sqlite3_connection.row_factory = simple_factory
+
+ select_query = "SELECT * FROM albums WHERE AlbumId = 1"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchall()
+ sqlite3_results = sqlite3_cursor.fetchall()
+
+ assert sqlitecloud_results == sqlite3_results
+ assert sqlitecloud_results[0]["Title"] == sqlite3_results[0]["Title"]
+
+ def test_description(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT * FROM albums WHERE AlbumId = 1"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ assert sqlitecloud_cursor.description == sqlite3_cursor.description
+ assert sqlitecloud_cursor.description[1][0] == "Title"
+ assert sqlite3_cursor.description[1][0] == "Title"
+
+ def test_fetch_one(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT * FROM albums WHERE AlbumId = 1"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ sqlitecloud_result = sqlitecloud_cursor.fetchone()
+ sqlite3_result = sqlite3_cursor.fetchone()
+
+ assert sqlitecloud_result == sqlite3_result
+
+ sqlitecloud_result = sqlitecloud_cursor.fetchone()
+ sqlite3_result = sqlite3_cursor.fetchone()
+
+ assert sqlitecloud_result is None
+ assert sqlite3_result is None
+
+ def test_fatchmany(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT * FROM albums"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchmany(2)
+ sqlite3_results = sqlite3_cursor.fetchmany(2)
+
+ assert len(sqlitecloud_results) == 2
+ assert len(sqlite3_results) == 2
+ assert sqlitecloud_results == sqlite3_results
+
+ def test_fetchmany_more_then_available(
+ self, sqlitecloud_dbapi2_connection, sqlite3_connection
+ ):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT * FROM albums LIMIT 3"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchmany(100)
+ sqlite3_results = sqlite3_cursor.fetchmany(100)
+
+ assert sqlitecloud_results == sqlite3_results
+ assert len(sqlitecloud_results) == 3
+ assert len(sqlite3_results) == 3
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchmany(100)
+ sqlite3_results = sqlite3_cursor.fetchmany(100)
+
+ assert sqlitecloud_results == sqlite3_results
+ assert len(sqlitecloud_results) == 0
+ assert len(sqlite3_results) == 0
+
+ def test_fetchall(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
+ sqlitecloud_connection = sqlitecloud_dbapi2_connection
+
+ select_query = "SELECT * FROM albums LIMIT 5"
+ sqlitecloud_cursor = sqlitecloud_connection.execute(select_query)
+ sqlite3_cursor = sqlite3_connection.execute(select_query)
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchall()
+ sqlite3_results = sqlite3_cursor.fetchall()
+
+ assert sqlitecloud_results == sqlite3_results
+ assert len(sqlitecloud_results) == 5
+ assert len(sqlite3_results) == 5
+
+ sqlitecloud_results = sqlitecloud_cursor.fetchall()
+ sqlite3_results = sqlite3_cursor.fetchall()
+
+ assert sqlitecloud_results == sqlite3_results
+ assert len(sqlitecloud_results) == 0
+ assert len(sqlite3_results) == 0
diff --git a/src/tests/unit/test_client.py b/src/tests/unit/test_client.py
index 844a456..7a561cc 100644
--- a/src/tests/unit/test_client.py
+++ b/src/tests/unit/test_client.py
@@ -1,10 +1,10 @@
-from sqlitecloud.client import SqliteCloudClient
+from sqlitecloud.client import SQLiteCloudClient
class TestClient:
def test_parse_connection_string_with_apikey(self):
connection_string = "sqlitecloud://user:pass@host.com:8860/dbname?apikey=abc123&timeout=10&compression=true"
- client = SqliteCloudClient(connection_str=connection_string)
+ client = SQLiteCloudClient(connection_str=connection_string)
assert not client.config.account.username
assert not client.config.account.password
@@ -17,7 +17,7 @@ def test_parse_connection_string_with_apikey(self):
def test_parse_connection_string_with_credentials(self):
connection_string = "sqlitecloud://user:pass@host.com:8860"
- client = SqliteCloudClient(connection_str=connection_string)
+ client = SQLiteCloudClient(connection_str=connection_string)
assert "user" == client.config.account.username
assert "pass" == client.config.account.password
@@ -27,7 +27,7 @@ def test_parse_connection_string_with_credentials(self):
def test_parse_connection_string_without_credentials(self):
connection_string = "sqlitecloud://host.com"
- client = SqliteCloudClient(connection_str=connection_string)
+ client = SQLiteCloudClient(connection_str=connection_string)
assert not client.config.account.username
assert not client.config.account.password
@@ -36,7 +36,7 @@ def test_parse_connection_string_without_credentials(self):
def test_parse_connection_string_with_all_parameters(self):
connection_string = "sqlitecloud://host.com:8860/dbname?apikey=abc123&compression=true&zerotext=true&memory=true&create=true&non_linearizable=true&insecure=true&no_verify_certificate=true&root_certificate=rootcert&certificate=cert&certificate_key=certkey&noblob=true&maxdata=10&maxrows=11&maxrowset=12"
- client = SqliteCloudClient(connection_str=connection_string)
+ client = SQLiteCloudClient(connection_str=connection_string)
assert "host.com" == client.config.account.hostname
assert 8860 == client.config.account.port
diff --git a/src/tests/unit/test_dbapi2.py b/src/tests/unit/test_dbapi2.py
new file mode 100644
index 0000000..c76aad5
--- /dev/null
+++ b/src/tests/unit/test_dbapi2.py
@@ -0,0 +1,334 @@
+import pytest
+from pytest_mock import MockerFixture
+
+import sqlitecloud
+from sqlitecloud import Cursor
+from sqlitecloud.dbapi2 import Connection
+from sqlitecloud.driver import Driver
+from sqlitecloud.resultset import SQLiteCloudResult
+from sqlitecloud.types import (
+ SQLITECLOUD_RESULT_TYPE,
+ SQLiteCloudAccount,
+ SQLiteCloudConfig,
+ SQLiteCloudException,
+)
+
+
+def test_connect_with_account_and_config(mocker: MockerFixture):
+ mock_connect = mocker.patch("sqlitecloud.driver.Driver.connect")
+
+ account = SQLiteCloudAccount()
+ account.hostname = "myhost"
+ account.port = 1234
+
+ config = SQLiteCloudConfig()
+ config.timeout = 99
+ config.memory = True
+
+ sqlitecloud.connect(account, config)
+
+ mock_connect.assert_called_once_with(account.hostname, account.port, config)
+
+
+def test_connect_with_connection_string_and_parameters(mocker: MockerFixture):
+ mock_connect = mocker.patch("sqlitecloud.driver.Driver.connect")
+
+ sqlitecloud.connect(
+ "sqlitecloud://user:pass@myhost:1234/dbname?timeout=99&memory=true"
+ )
+
+ mock_connect.assert_called_once()
+ assert mock_connect.call_args[0][0] == "myhost"
+ assert mock_connect.call_args[0][1] == 1234
+ # config
+ assert mock_connect.call_args[0][2].timeout == 99
+ assert mock_connect.call_args[0][2].memory
+
+
+class TestCursor:
+ def test_description_empty(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ description = cursor.description
+
+ assert description is None
+
+ def test_description_with_resultset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 1
+ result.data = ["myname"]
+ result.colname = ["column1"]
+ cursor._resultset = result
+
+ assert cursor.description == (("column1", None, None, None, None, None, None),)
+
+ def test_description_with_resultset_multiple_rows(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 2
+ result.nrows = 1
+ result.data = ["myname"]
+ result.colname = ["name", "id"]
+ cursor._resultset = result
+
+ assert cursor.description == (
+ ("name", None, None, None, None, None, None),
+ ("id", None, None, None, None, None, None),
+ )
+
+ def test_rowcount_with_rowset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 3
+ result.data = ["myname1", "myname2", "myname3"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.rowcount == 3
+
+ def test_rowcount_with_result(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_BLOB)
+ cursor._resultset = result
+
+ assert cursor.rowcount == -1
+
+ def test_rowcount_with_no_resultset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ assert cursor.rowcount == -1
+
+ def test_execute_escaped(self, mocker: MockerFixture):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+ execute_mock = mocker.patch.object(Driver, "execute")
+
+ sql = "SELECT * FROM users WHERE name = ?"
+ parameters = ("John's",)
+
+ cursor.execute(sql, parameters)
+
+ assert (
+ execute_mock.call_args[0][0] == "SELECT * FROM users WHERE name = 'John''s'"
+ )
+
+ def test_executemany(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+ execute_mock = mocker.patch.object(cursor, "execute")
+
+ sql = "INSERT INTO users (name, age) VALUES (?, ?)"
+ seq_of_parameters = [("John", 25), ("Jane", 30), ("Bob", 40)]
+
+ cursor.executemany(sql, seq_of_parameters)
+
+ execute_mock.assert_called_once_with(
+ "INSERT INTO users (name, age) VALUES ('John', 25);INSERT INTO users (name, age) VALUES ('Jane', 30);INSERT INTO users (name, age) VALUES ('Bob', 40);"
+ )
+
+ def test_executemany_escaped(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+ execute_mock = mocker.patch.object(cursor, "execute")
+
+ sql = "INSERT INTO users (name, age) VALUES (?, ?)"
+ seq_of_parameters = [("O'Conner", 25)]
+
+ cursor.executemany(sql, seq_of_parameters)
+
+ execute_mock.assert_called_once_with(
+ "INSERT INTO users (name, age) VALUES ('O''Conner', 25);"
+ )
+
+ def test_fetchone_with_no_resultset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ assert cursor.fetchone() is None
+
+ def test_fetchone_with_result(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_BLOB)
+ cursor._resultset = result
+
+ assert cursor.fetchone() is None
+
+ def test_fetchone_with_rowset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 1
+ result.data = ["myname"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchone() == ("myname",)
+
+ def test_fetchone_twice(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 1
+ result.data = ["myname"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchone() is not None
+ assert cursor.fetchone() is None
+
+ def test_fetchmany_with_no_resultset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ assert cursor.fetchmany() == []
+
+ def test_fetchmany_with_result(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_BLOB)
+ cursor._resultset = result
+
+ assert cursor.fetchmany() == []
+
+ def test_fetchmany_with_rowset_and_default_size(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 3
+ result.data = ["myname1", "myname2", "myname3"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchmany(None) == [("myname1",)]
+
+ def test_fetchmany_twice_to_retrieve_whole_rowset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 2
+ result.data = ["myname1", "myname2"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchmany(2) == [("myname1",), ("myname2",)]
+ assert cursor.fetchmany() == []
+
+ def test_fetchmany_with_size_higher_than_rowcount(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 1
+ result.data = ["myname1"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchmany(2) == [("myname1",)]
+
+ def test_fetchall_with_no_resultset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ assert cursor.fetchall() == []
+
+ def test_fetchall_with_result(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_BLOB)
+ cursor._resultset = result
+
+ assert cursor.fetchall() == []
+
+ def test_fetchall_with_rowset(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 3
+ result.data = ["myname1", "myname2", "myname3"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchall() == [("myname1",), ("myname2",), ("myname3",)]
+
+ def test_fetchall_twice_and_expect_empty_list(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 2
+ result.data = ["myname1", "myname2"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchall() == [("myname1",), ("myname2",)]
+ assert cursor.fetchall() == []
+
+ def test_fetchall_to_return_remaining_rows(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 2
+ result.data = ["myname1", "myname2"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert cursor.fetchone() is not None
+ assert cursor.fetchall() == [("myname2",)]
+
+ def test_iterator(self, mocker):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 2
+ result.data = ["myname1", "myname2"]
+ result.colname = ["name"]
+ cursor._resultset = result
+
+ assert list(cursor) == [("myname1",), ("myname2",)]
+
+ def test_row_factory(self, mocker):
+ conn = Connection(mocker.patch("sqlitecloud.types.SQLiteCloudConnect"))
+ conn.row_factory = lambda x, y: {"name": y[0]}
+
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 1
+ result.nrows = 2
+ result.data = ["myname1", "myname2"]
+ result.colname = ["name"]
+
+ cursor = conn.cursor()
+ cursor._resultset = result
+
+ assert cursor.fetchone() == {"name": "myname1"}
+
+ @pytest.mark.parametrize(
+ "method, args",
+ [
+ ("execute", ("",)),
+ ("executemany", ("", [])),
+ ("fetchone", ()),
+ ("fetchmany", ()),
+ ("fetchall", ()),
+ ("close", ()),
+ ],
+ )
+ def test_close_raises_expected_exception_on_any_further_operation(
+ self, method, args, mocker
+ ):
+ cursor = Cursor(mocker.patch("sqlitecloud.Connection"))
+
+ cursor.close()
+
+ with pytest.raises(SQLiteCloudException) as e:
+ getattr(cursor, method)(*args)
+
+ assert e.value.args[0] == "The cursor is closed."
diff --git a/src/tests/unit/test_driver.py b/src/tests/unit/test_driver.py
index 8e6d819..f49ba32 100644
--- a/src/tests/unit/test_driver.py
+++ b/src/tests/unit/test_driver.py
@@ -1,6 +1,8 @@
import pytest
+from pytest_mock import MockerFixture
from sqlitecloud.driver import Driver
+from sqlitecloud.types import SQLiteCloudAccount, SQLiteCloudConfig
class TestDriver:
@@ -89,3 +91,168 @@ def test_parse_rowset_signature(self):
assert 1 == result.version
assert 1 == result.nrows
assert 2 == result.ncols
+
+ def test_prepare_statement_with_tuple_parameters(self):
+ driver = Driver()
+
+ query = "SELECT * FROM users WHERE age > ? AND name = ?"
+ parameters = (18, "John")
+
+ expected_result = "SELECT * FROM users WHERE age > 18 AND name = 'John'"
+ result = driver.prepare_statement(query, parameters)
+
+ assert expected_result == result
+
+ def test_prepare_statement_with_dict_parameters(self):
+ driver = Driver()
+
+ query = "INSERT INTO users (name, age) VALUES (:name, :age)"
+ parameters = {"name": "Alice", "age": 25}
+
+ expected_result = "INSERT INTO users (name, age) VALUES ('Alice', 25)"
+ result = driver.prepare_statement(query, parameters)
+
+ assert expected_result == result
+
+ def test_prepare_statement_with_missing_parameters_does_not_raise_exception(self):
+ driver = Driver()
+
+ query = "UPDATE users SET name = :name, age = :age WHERE id = :id"
+ parameters = {"name": "Bob"}
+
+ expected_result = "UPDATE users SET name = 'Bob', age = :age WHERE id = :id"
+
+ result = driver.prepare_statement(query, parameters)
+
+ assert expected_result == result
+
+ def test_prepare_statement_with_extra_parameters(self):
+ driver = Driver()
+
+ query = "SELECT * FROM users WHERE age > :age"
+ parameters = {"age": 30, "name": "Alice"}
+
+ expected_result = "SELECT * FROM users WHERE age > 30"
+
+ result = driver.prepare_statement(query, parameters)
+
+ assert expected_result == result
+
+ def test_prepare_statement_with_sql_injection_threat(self):
+ driver = Driver()
+
+ query = "SELECT * FROM phone WHERE name = ?"
+ parameter = ("Jack's phone; DROP TABLE phone;",)
+
+ expected_result = (
+ "SELECT * FROM phone WHERE name = 'Jack''s phone; DROP TABLE phone;'"
+ )
+ result = driver.prepare_statement(query, parameter)
+
+ assert expected_result == result
+
+ def test_escape_sql_parameter_with_string(self):
+ driver = Driver()
+ param = "John's SQL"
+
+ expected_result = "'John''s SQL'"
+ result = driver.escape_sql_parameter(param)
+
+ assert expected_result == result
+
+ def test_escape_sql_parameter_with_integer(self):
+ driver = Driver()
+ param = 123
+
+ expected_result = "123"
+ result = driver.escape_sql_parameter(param)
+
+ assert expected_result == result
+
+ def test_escape_sql_parameter_with_float(self):
+ driver = Driver()
+ param = 3.14
+
+ expected_result = "3.14"
+ result = driver.escape_sql_parameter(param)
+
+ assert expected_result == result
+
+ def test_escape_sql_parameter_with_none(self):
+ driver = Driver()
+ param = None
+
+ expected_result = "NULL"
+ result = driver.escape_sql_parameter(param)
+
+ assert expected_result == result
+
+ def test_escape_sql_parameter_with_bool(self):
+ driver = Driver()
+ param = True
+
+ expected_result = "1"
+ result = driver.escape_sql_parameter(param)
+
+ assert expected_result == result
+
+ def test_escape_sql_parameter_with_bytes(self):
+ driver = Driver()
+ param = b"Hello"
+
+ expected_result = "X'48656c6c6f'"
+ result = driver.escape_sql_parameter(param)
+
+ assert expected_result == result
+
+ def test_escape_sql_parameter_with_dict(self):
+ driver = Driver()
+ param = {"name": "O'Conner", "age": 25}
+
+ expected_result = '\'{"name": "O\'\'Conner", "age": 25}\''
+ driver.escape_sql_parameter(param)
+
+ assert expected_result
+
+ def test_nonlinearizable_command_before_auth_with_account(
+ self, mocker: MockerFixture
+ ):
+ driver = Driver()
+
+ config = SQLiteCloudConfig()
+ config.account = SQLiteCloudAccount()
+ config.account.username = "pippo"
+ config.account.password = "pluto"
+ config.non_linearizable = True
+
+ mocker.patch.object(driver, "_internal_connect", return_value=None)
+ run_command_mock = mocker.patch.object(driver, "_internal_run_command")
+
+ driver.connect("myhost", 8860, config)
+
+ expected_buffer = (
+ "SET CLIENT KEY NONLINEARIZABLE TO 1;AUTH USER pippo PASSWORD pluto;"
+ )
+
+ run_command_mock.assert_called_once()
+ assert run_command_mock.call_args[0][1] == expected_buffer
+
+ def test_nonlinearizable_command_before_auth_with_apikey(
+ self, mocker: MockerFixture
+ ):
+ driver = Driver()
+
+ config = SQLiteCloudConfig()
+ config.account = SQLiteCloudAccount()
+ config.account.apikey = "abc123"
+ config.non_linearizable = True
+
+ mocker.patch.object(driver, "_internal_connect", return_value=None)
+ run_command_mock = mocker.patch.object(driver, "_internal_run_command")
+
+ driver.connect("myhost", 8860, config)
+
+ expected_buffer = "SET CLIENT KEY NONLINEARIZABLE TO 1;AUTH APIKEY abc123;"
+
+ run_command_mock.assert_called_once()
+ assert run_command_mock.call_args[0][1] == expected_buffer
diff --git a/src/tests/unit/test_resultset.py b/src/tests/unit/test_resultset.py
index 28b1264..f89f274 100644
--- a/src/tests/unit/test_resultset.py
+++ b/src/tests/unit/test_resultset.py
@@ -1,12 +1,12 @@
import pytest
-from sqlitecloud.resultset import SQCloudResult, SqliteCloudResultSet
-from sqlitecloud.types import SQCLOUD_RESULT_TYPE
+from sqlitecloud.resultset import SQLiteCloudResult, SQLiteCloudResultSet
+from sqlitecloud.types import SQLITECLOUD_RESULT_TYPE, SQLITECLOUD_VALUE_TYPE
-class TestSqCloudResult:
+class TestSQLiteCloudResult:
def test_init_data(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_INTEGER)
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_INTEGER)
result.init_data(42)
assert 1 == result.nrows
assert 1 == result.ncols
@@ -14,7 +14,7 @@ def test_init_data(self):
assert True is result.is_result
def test_init_data_with_array(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_ARRAY)
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ARRAY)
result.init_data([42, 43, 44])
assert 1 == result.nrows
@@ -23,37 +23,111 @@ def test_init_data_with_array(self):
assert True is result.is_result
def test_init_as_dataset(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
assert False is result.is_result
assert 0 == result.nrows
assert 0 == result.ncols
assert 0 == result.version
+ def test_get_value_with_rowset(self):
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.nrows = 2
+ result.ncols = 2
+ result.colname = ["name", "age"]
+ result.data = ["John", 42, "Doe", 24]
+ result.version = 2
+
+ assert "John" == result.get_value(0, 0)
+ assert 24 == result.get_value(1, 1)
+ assert result.get_value(2, 2) is None
+
+ def test_get_value_array(self):
+ result = SQLiteCloudResult(
+ SQLITECLOUD_RESULT_TYPE.RESULT_ARRAY, result=[1, 2, 3]
+ )
+
+ assert [1, 2, 3] == result.get_value(0, 0)
+
+ def test_get_colname(self):
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.ncols = 2
+ result.colname = ["name", "age"]
+
+ assert "name" == result.get_name(0)
+ assert "age" == result.get_name(1)
+ assert result.get_name(2) is None
+
+ def test_get_value_with_empty_decltype(self):
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.nrows = 2
+ result.ncols = 2
+ result.colname = []
+ result.decltype = []
+ result.data = ["John", "42", "Doe", "24"]
+
+ assert "John" == result.get_value(0, 0)
+ assert "42" == result.get_value(0, 1)
+ assert "Doe" == result.get_value(1, 0)
+ assert "24" == result.get_value(1, 1)
+
+ def test_get_value_with_convert_false(self):
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.nrows = 1
+ result.ncols = 2
+ result.colname = ["name", "age"]
+ result.data = ["John", "42"]
+ result.decltype = ["TEXT", "INTEGER"]
+
+ assert "John" == result.get_value(0, 0, convert=False)
+ assert "42" == result.get_value(0, 1, convert=False)
+
+ @pytest.mark.parametrize(
+ "value_type, value, expected_value",
+ [
+ (SQLITECLOUD_VALUE_TYPE.INTEGER.value, "24", 24),
+ (SQLITECLOUD_VALUE_TYPE.FLOAT.value, "3.14", 3.14),
+ (SQLITECLOUD_VALUE_TYPE.TEXT.value, "John", "John"),
+ (SQLITECLOUD_VALUE_TYPE.BLOB.value, b"hello", b"hello"),
+ (SQLITECLOUD_VALUE_TYPE.NULL.value, "NULL", None),
+ ],
+ )
+ def test_get_value_to_convert_text(self, value_type, value, expected_value):
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
+ result.nrows = 1
+ result.ncols = 1
+ result.colname = ["mycol"]
+ result.data = [value]
+ result.decltype = [value_type]
+
+ result_set = SQLiteCloudResultSet(result)
+
+ assert expected_value == result_set.get_value(0, 0)
+
class TestSqliteCloudResultSet:
def test_next(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_INTEGER, result=42)
- result_set = SqliteCloudResultSet(result)
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_INTEGER, result=42)
+ result_set = SQLiteCloudResultSet(result)
assert {"result": 42} == next(result_set)
with pytest.raises(StopIteration):
next(result_set)
def test_iter_result(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_INTEGER, result=42)
- result_set = SqliteCloudResultSet(result)
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_INTEGER, result=42)
+ result_set = SQLiteCloudResultSet(result)
for row in result_set:
assert {"result": 42} == row
def test_iter_rowset(self):
- rowset = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_ROWSET)
+ rowset = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET)
rowset.nrows = 2
rowset.ncols = 2
rowset.colname = ["name", "age"]
rowset.data = ["John", 42, "Doe", 24]
rowset.version = 2
- result_set = SqliteCloudResultSet(rowset)
+ result_set = SQLiteCloudResultSet(rowset)
out = []
for row in result_set:
@@ -63,37 +137,8 @@ def test_iter_rowset(self):
assert {"name": "John", "age": 42} == out[0]
assert {"name": "Doe", "age": 24} == out[1]
- def test_get_value_with_rowset(self):
- rowset = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_ROWSET)
- rowset.nrows = 2
- rowset.ncols = 2
- rowset.colname = ["name", "age"]
- rowset.data = ["John", 42, "Doe", 24]
- rowset.version = 2
- result_set = SqliteCloudResultSet(rowset)
-
- assert "John" == result_set.get_value(0, 0)
- assert 24 == result_set.get_value(1, 1)
- assert result_set.get_value(2, 2) is None
-
- def test_get_value_array(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_ARRAY, result=[1, 2, 3])
- result_set = SqliteCloudResultSet(result)
-
- assert [1, 2, 3] == result_set.get_value(0, 0)
-
- def test_get_colname(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_ROWSET)
- result.ncols = 2
- result.colname = ["name", "age"]
- result_set = SqliteCloudResultSet(result)
-
- assert "name" == result_set.get_name(0)
- assert "age" == result_set.get_name(1)
- assert result_set.get_name(2) is None
-
def test_get_result_with_single_value(self):
- result = SQCloudResult(SQCLOUD_RESULT_TYPE.RESULT_INTEGER, result=42)
- result_set = SqliteCloudResultSet(result)
+ result = SQLiteCloudResult(SQLITECLOUD_RESULT_TYPE.RESULT_INTEGER, result=42)
+ result_set = SQLiteCloudResultSet(result)
assert 42 == result_set.get_result()
diff --git a/src/tests/unit/test_types.py b/src/tests/unit/test_types.py
new file mode 100644
index 0000000..e08e569
--- /dev/null
+++ b/src/tests/unit/test_types.py
@@ -0,0 +1,19 @@
+import pytest
+
+from sqlitecloud.types import SQLiteCloudConfig
+
+
+class TestSQLiteCloudConfig:
+ @pytest.mark.parametrize(
+ "param, value",
+ [
+ ("non_linearizable", True),
+ ("nonlinearizable", True),
+ ],
+ )
+ def test_parse_connection_string_with_nonlinarizable(self, param: str, value: any):
+ connection_string = f"sqlitecloud://myhost.sqlitecloud.io?{param}={value}"
+
+ config = SQLiteCloudConfig(connection_string)
+
+ assert config.non_linearizable