Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- name: Harden runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ build/
coverage.xml
htmlcov/
.pytest_cache/
.mypy_cache/

# IDE
.vscode/
Expand Down
70 changes: 70 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Contributing to MiniDB

## Prerequisites

- Python 3.11 or later
- No external dependencies for the library itself

## Development Setup

```bash
git clone https://github.com/SebTardif/MiniDB.git
cd MiniDB

# Install dev tools
pip install ruff mypy pytest pytest-cov

# Verify everything works
python -m pytest tests/ -v
python main.py
```

## Pre-Commit Checks

Run these before submitting a PR (CI enforces all of them):

```bash
# Lint
ruff check .

# Format
ruff format --check .

# Type check
mypy minidb/

# Tests
python -m pytest tests/ -v
```

## Commit Message Convention

This project uses [Conventional Commits](https://www.conventionalcommits.org/)
for automatic versioning via semantic-release.

Allowed prefixes: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`,
`test`, `build`, `ci`, `chore`, `revert`.

Examples:
- `feat: add DISTINCT keyword support`
- `fix: handle NULL in ORDER BY correctly`
- `test: add edge case tests for JOIN`
- `docs: update SQL syntax reference`

## Project Structure

```
minidb/
database.py # MiniDB main class (facade)
parser.py # SQL lexer and parser
query.py # Query execution engine
table.py # Table storage and row management
column.py # Column and Schema definitions
index.py # Hash-based indexing
planner.py # Query planner (index vs table scan)
persistence.py # JSON serialization
types.py # Type definitions and enums
errors.py # Custom exception hierarchy
tests/
test_*.py # Test files matching source modules
```
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A miniature in-memory database with SQL-like query support, built from scratch u
- GROUP BY with aggregations
- LIMIT clause
- **Aggregations**: COUNT, SUM, AVG, MIN, MAX
- **JOINs**: INNER JOIN between tables
- **JOINs**: INNER JOIN and LEFT JOIN between tables
- **Indexing**: Automatic hash-based indexing on primary keys
- **Query Planner**: Chooses between index scans and table scans
- **Persistence**: Save/load database to JSON files with versioning
Expand All @@ -23,7 +23,7 @@ No installation required! MiniDB uses only Python standard library.

```bash
# Just clone and use
git clone <repo-url>
git clone https://github.com/SebTardif/MiniDB.git
cd MiniDB
python -m pytest tests/ -v # Run tests
python main.py # Run demo
Expand Down Expand Up @@ -161,6 +161,11 @@ LIMIT 10
SELECT t1.col, t2.col
FROM t1
JOIN t2 ON t1.id = t2.t1_id

-- LEFT JOIN (includes unmatched left rows with NULLs)
SELECT t1.col, t2.col
FROM t1
LEFT JOIN t2 ON t1.id = t2.t1_id
```

### UPDATE
Expand Down Expand Up @@ -194,7 +199,7 @@ minidb/

## Test Suite

MiniDB includes a comprehensive test suite with 29+ test cases:
MiniDB includes a comprehensive test suite with 95+ test cases:

```bash
# Run all tests
Expand All @@ -211,7 +216,7 @@ python -m pytest tests/ -v --cov=minidb

- **test_database.py**: Database lifecycle and table management
- **test_crud.py**: INSERT, SELECT, UPDATE, DELETE operations
- **test_queries.py**: WHERE, ORDER BY, LIMIT
- **test_queries.py**: WHERE, ORDER BY, LIMIT, parser errors, type validation
- **test_aggregations.py**: COUNT, SUM, AVG, MIN, MAX, GROUP BY
- **test_joins.py**: JOIN operations
- **test_index.py**: Indexing and query planning
Expand All @@ -233,16 +238,17 @@ MiniDB is designed for small to medium datasets:
- Single-threaded
- No transactions
- No foreign key constraints
- No NULL handling in some edge cases
- Limited JOIN support (INNER JOIN only)
- Limited JOIN support (INNER and LEFT JOIN only, no RIGHT JOIN execution)

## License

MIT License - Use freely for any purpose.

## Contributing

Contributions welcome! Areas for improvement:
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.

Areas for improvement:

- B-tree indexes for range queries
- LEFT/RIGHT OUTER JOIN
Expand Down
4 changes: 0 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
- Persistence
"""

import sys

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import MiniDB


Expand Down
11 changes: 3 additions & 8 deletions minidb/query.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Query execution engine for MiniDB."""

import re
from collections import defaultdict
from typing import Any

Expand Down Expand Up @@ -241,7 +242,6 @@ def _evaluate_condition(self, row: Row, condition: Condition) -> bool:

def _match_like(self, value: str, pattern: str) -> bool:
"""Match a value against a LIKE pattern."""
import re

# Convert SQL LIKE pattern to regex
# % matches any sequence, _ matches single character
Expand Down Expand Up @@ -385,13 +385,8 @@ def sort_key(row):
return keys

# Sort with direction handling
sorted_rows = sorted(rows, key=sort_key)

# Reverse if DESC
if order_by and order_by[0].direction == 'DESC':
sorted_rows = sorted(rows, key=sort_key, reverse=True)

return sorted_rows
reverse = bool(order_by and order_by[0].direction == 'DESC')
return sorted(rows, key=sort_key, reverse=reverse)

def execute_insert(self, query: InsertQuery) -> int:
"""Execute an INSERT query."""
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Database",
"Typing :: Typed",
]
Expand Down
4 changes: 0 additions & 4 deletions tests/test_aggregations.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"""Tests for aggregation functions."""

import sys

import pytest

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import Column, ColumnType, MiniDB


Expand Down
4 changes: 0 additions & 4 deletions tests/test_crud.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"""Tests for INSERT, SELECT, UPDATE, DELETE operations."""

import sys

import pytest

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import Column, ColumnType, DuplicateKeyError, MiniDB


Expand Down
53 changes: 49 additions & 4 deletions tests/test_database.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"""Tests for database lifecycle and table management."""

import sys

import pytest

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import Column, ColumnType, MiniDB, TableExistsError, TableNotFoundError


Expand Down Expand Up @@ -99,3 +95,52 @@ def test_drop_table_not_found(self):

with pytest.raises(TableNotFoundError):
db.drop_table('nonexistent')

def test_drop_table_sql(self):
"""Test dropping a table using SQL."""
db = MiniDB()
db.execute('CREATE TABLE temp (id INTEGER PRIMARY KEY, value STRING)')
assert 'temp' in db
db.execute('DROP TABLE temp')
assert 'temp' not in db

def test_query_on_non_select(self):
"""Test that query() raises MiniDBError for non-SELECT statements."""
from minidb.errors import MiniDBError

db = MiniDB()
db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, v STRING)')
with pytest.raises(MiniDBError, match='Query did not return rows'):
db.query("INSERT INTO t (id, v) VALUES (1, 'x')")

def test_repr(self):
"""Test __repr__ on MiniDB."""
db = MiniDB()
assert repr(db) == 'MiniDB()'
db.execute('CREATE TABLE users (id INTEGER PRIMARY KEY)')
db.execute('INSERT INTO users (id) VALUES (1)')
assert 'users(1 rows)' in repr(db)

def test_insert_into_nonexistent_table(self):
"""Test INSERT into a table that does not exist raises error."""
db = MiniDB()
with pytest.raises(TableNotFoundError):
db.execute('INSERT INTO ghost (id) VALUES (1)')

def test_update_nonexistent_table(self):
"""Test UPDATE on a table that does not exist raises error."""
db = MiniDB()
with pytest.raises(TableNotFoundError):
db.execute('UPDATE ghost SET id = 1')

def test_delete_from_nonexistent_table(self):
"""Test DELETE from a table that does not exist raises error."""
db = MiniDB()
with pytest.raises(TableNotFoundError):
db.execute('DELETE FROM ghost')

def test_select_from_nonexistent_table(self):
"""Test SELECT from a table that does not exist raises error."""
db = MiniDB()
with pytest.raises(TableNotFoundError):
db.query('SELECT * FROM ghost')
4 changes: 0 additions & 4 deletions tests/test_index.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
"""Tests for indexing and query planning."""

import sys

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import Column, ColumnType, HashIndex, MiniDB
from minidb.planner import QueryPlanner, ScanType

Expand Down
4 changes: 0 additions & 4 deletions tests/test_joins.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"""Tests for JOIN operations."""

import sys

import pytest

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import Column, ColumnType, MiniDB


Expand Down
3 changes: 0 additions & 3 deletions tests/test_performance.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
"""Tests for performance with large datasets."""

import sys
import time

import pytest

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import Column, ColumnType, MiniDB


Expand Down
43 changes: 40 additions & 3 deletions tests/test_persistence.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
"""Tests for persistence functionality."""

import os
import sys
import tempfile

import pytest

sys.path.insert(0, '/home/sebtardif/MiniDB')

from minidb import Column, ColumnType, MiniDB


Expand Down Expand Up @@ -170,6 +167,46 @@ def test_persistence_file_not_found(self):
with pytest.raises(FileReadError):
MiniDB.load('/nonexistent/path/to/file.json')

def test_persistence_version_mismatch(self):
"""Test loading a file with incompatible version raises error."""
import json

from minidb.errors import VersionMismatchError

with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
filepath = f.name
json.dump({'version': '99.0', 'tables': {}}, f)

try:
with pytest.raises(VersionMismatchError) as exc_info:
MiniDB.load(filepath)
assert '99.0' in str(exc_info.value)
finally:
os.unlink(filepath)

def test_persistence_invalid_json(self):
"""Test loading a file with invalid JSON raises error."""
from minidb.errors import FileReadError

with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
filepath = f.name
f.write('not valid json {{{')

try:
with pytest.raises(FileReadError) as exc_info:
MiniDB.load(filepath)
assert 'Invalid JSON' in str(exc_info.value)
finally:
os.unlink(filepath)

def test_persistence_write_to_readonly_path(self):
"""Test saving to an unwritable path raises error."""
from minidb.errors import FileWriteError

db = MiniDB()
with pytest.raises(FileWriteError):
db.save('/nonexistent/directory/file.json')

def test_json_format_readable(self):
"""Test that saved JSON is human-readable."""
db = MiniDB()
Expand Down
Loading