Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add delete() method to QueryBuilder with comprehensive test coverage #61

Merged
merged 4 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand All @@ -29,8 +29,7 @@ jobs:

- name: Run tests
# For example, using `pytest`
run:
uv run --all-extras -p ${{ matrix.python-version }} pytest tests
run: uv run --all-extras -p ${{ matrix.python-version }} pytest tests
--cov-report=xml

- name: Run codacy-coverage-reporter
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,11 @@ for user in results:
new_user.age = 31
db.update(new_user)

# Delete a record
# Delete a record by primary key
db.delete(User, new_user.pk)

# Delete all records returned from a query:
delete_count = db.select(User).filter(age__gt=30).delete()
```

See the [Usage](https://sqliter.grantramsay.dev/usage) section of the documentation
Expand Down
4 changes: 0 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ Items marked with :fire: are high priority.
- add an 'execute' method to the main class to allow executing arbitrary SQL
queries which can be chained to the 'find_first' etc methods or just used
directly.
- add a `delete` method to the QueryBuilder class to allow deleting
single/multiple records from the database based on the query. This is in
addition to the `delete` method in the main class which deletes a single
record based on the primary key.
- add a `rollback` method to the main class to allow manual rollbacks.
- :fire: allow adding foreign keys and relationships to each table.
- add a migration system to allow updating the database schema without losing
Expand Down
39 changes: 37 additions & 2 deletions docs/guide/data-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,48 @@ timestamp in UTC by default.

## Deleting Records

To delete a record from the database, you need to pass the model class and the
primary key value of the record you want to delete:
SQLiter provides two ways to delete records:

### Single Record Deletion

To delete a single record from the database by its primary key, use the `delete()` method directly on the database instance:

```python
db.delete(User, user.pk)
```

> [!IMPORTANT]
>
> The single record deletion method will raise:
>
> - `RecordNotFoundError` if the record with the specified primary key is not found
> - `RecordDeletionError` if there's an error during the deletion process

### Query-Based Deletion

You can also use a query to delete records that match specific criteria. The `delete()` method will delete all records returned by the query and return an integer with the count of records deleted:

```python
# Delete all users over 30
deleted_count = db.select(User).filter(age__gt=30).delete()

# Delete inactive users in a specific age range
deleted_count = db.select(User).filter(
age__gte=25,
age__lt=40,
status="inactive"
).delete()

# Delete all records from a table
deleted_count = db.select(User).delete()
```

> [!NOTE]
>
> The query-based delete operation ignores any `limit()`, `offset()`, or `order()`
> clauses that might be in the query chain. It will always delete ALL records
> that match the filter conditions.

## Commit your changes

By default, SQLiter will automatically commit changes to the database after each
Expand Down
36 changes: 33 additions & 3 deletions docs/guide/guide.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# SQLiter Overview

SQLiter is a lightweight Python library designed to simplify database operations
Expand Down Expand Up @@ -122,13 +121,22 @@ db.update(user)

## Deleting Records

Deleting records is simple as well. You just need to pass the Model that defines
your table and the primary key value of the record you want to delete:
SQLiter provides two ways to delete records:

### Single Record Deletion

To delete a single record by its primary key:

```python
db.delete(User, 1)
```

> [!IMPORTANT]
>
> The single record deletion method will raise:
> - `RecordNotFoundError` if the record with the specified primary key is not found
> - `RecordDeletionError` if there's an error during the deletion process

> [!NOTE]
>
> You can get the primary key value from the record or model instance itself,
Expand All @@ -139,6 +147,28 @@ db.delete(User, 1)
> db.delete(User, new_record.pk)
> ```

### Query-Based Deletion

You can also delete multiple records that match specific criteria using a query. The `delete()` method will delete all records that match the query and return the number of records deleted:

```python
# Delete all users over 30
deleted_count = db.select(User).filter(age__gt=30).delete()

# Delete inactive users in a specific age range
deleted_count = db.select(User).filter(
age__gte=25,
age__lt=40,
status="inactive"
).delete()
```

> [!NOTE]
>
> The query-based delete operation ignores any `limit()`, `offset()`, or `order()`
> clauses that might be in the query chain. It will always delete ALL records
> that match the filter conditions.

## Advanced Query Features

### Ordering
Expand Down
5 changes: 4 additions & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ results = db.select(User).filter(name="John Doe").fetch_one()

print("Updated age:", results.age)

# Delete a record
# Delete a record by primary key
db.delete(User, new_user.pk)

# Delete all records returned from a query:
delete_count = db.select(User).filter(age__gt=30).delete()
```

See the [Guide](guide/guide.md) for more detailed information on how to use `SQLiter`.
32 changes: 32 additions & 0 deletions sqliter/query/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
InvalidFilterError,
InvalidOffsetError,
InvalidOrderError,
RecordDeletionError,
RecordFetchError,
)

Expand Down Expand Up @@ -728,3 +729,34 @@ def exists(self) -> bool:
True if at least one result exists, False otherwise.
"""
return self.count() > 0

def delete(self) -> int:
"""Delete records that match the current query conditions.

Returns:
The number of records deleted.

Raises:
RecordDeletionError: If there's an error deleting the records.
"""
sql = f'DELETE FROM "{self.table_name}"' # noqa: S608 # nosec

# Build the WHERE clause with special handling for None (NULL in SQL)
values, where_clause = self._parse_filter()

if self.filters:
sql += f" WHERE {where_clause}"

# Print the raw SQL and values if debug is enabled
if self.db.debug:
self.db._log_sql(sql, values) # noqa: SLF001

try:
with self.db.connect() as conn:
cursor = conn.cursor()
cursor.execute(sql, values)
deleted_count = cursor.rowcount
self.db._maybe_commit() # noqa: SLF001
return deleted_count
except sqlite3.Error as exc:
raise RecordDeletionError(self.table_name) from exc
Loading
Loading