From c94ee0cefbeb8ba0d2b90746da85fe5431658438 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 2 Jul 2026 12:00:12 -0700 Subject: [PATCH 1/6] test: add 17 tests covering error paths and edge cases Cover previously untested error paths: - Persistence: VersionMismatchError, invalid JSON, write to unwritable path - Parser: SyntaxError_ for malformed SQL, unterminated strings, unexpected chars, empty SQL - Database: query() on non-SELECT, INSERT/UPDATE/DELETE/SELECT on nonexistent table, DROP TABLE via SQL, __repr__, NULL on primary key, missing nullable columns, DuplicateKeyError message content Increases test count from 78 to 95. Signed-off-by: Sebastien Tardif --- tests/test_database.py | 49 ++++++++++++++++++++++++++++++ tests/test_persistence.py | 40 +++++++++++++++++++++++++ tests/test_queries.py | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) diff --git a/tests/test_database.py b/tests/test_database.py index 5626e57..b6f661d 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -99,3 +99,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') diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1103017..27b599c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -170,6 +170,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() diff --git a/tests/test_queries.py b/tests/test_queries.py index d3448d9..b7b94bd 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -7,6 +7,69 @@ sys.path.insert(0, '/home/sebtardif/MiniDB') from minidb import Column, ColumnType, MiniDB +from minidb.errors import SyntaxError_ + + +class TestParserErrors: + """Tests for SQL parsing error paths.""" + + def test_syntax_error_unexpected_token(self): + """Test that malformed SQL raises SyntaxError_.""" + db = MiniDB() + with pytest.raises(SyntaxError_): + db.execute('SELECTX * FROM users') + + def test_syntax_error_unterminated_string(self): + """Test that an unterminated string literal raises SyntaxError_.""" + db = MiniDB() + db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, v STRING)') + with pytest.raises(SyntaxError_, match='Unterminated string'): + db.execute("INSERT INTO t (id, v) VALUES (1, 'hello)") + + def test_syntax_error_unexpected_character(self): + """Test that unexpected characters raise SyntaxError_.""" + db = MiniDB() + with pytest.raises(SyntaxError_): + db.execute('SELECT * FROM t WHERE id @ 1') + + def test_empty_sql(self): + """Test that empty SQL raises SyntaxError_.""" + db = MiniDB() + with pytest.raises(SyntaxError_): + db.execute('') + + +class TestTypeValidation: + """Tests for type validation and NULL constraint errors.""" + + def test_null_value_on_primary_key(self): + """Test inserting NULL into a non-nullable primary key column.""" + from minidb.errors import NullValueError + + db = MiniDB() + db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, v STRING)') + with pytest.raises(NullValueError): + db.execute("INSERT INTO t (id, v) VALUES (NULL, 'hello')") + + def test_insert_with_missing_nullable_column(self): + """Test that missing nullable columns default to NULL.""" + db = MiniDB() + db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY, v STRING)') + db.execute('INSERT INTO t (id) VALUES (1)') + results = db.query('SELECT * FROM t WHERE id = 1') + assert len(results) == 1 + assert results[0]['v'] is None + + def test_duplicate_primary_key_error_message(self): + """Test that DuplicateKeyError includes the key value.""" + from minidb.errors import DuplicateKeyError + + db = MiniDB() + db.execute('CREATE TABLE t (id INTEGER PRIMARY KEY)') + db.execute('INSERT INTO t (id) VALUES (42)') + with pytest.raises(DuplicateKeyError) as exc_info: + db.execute('INSERT INTO t (id) VALUES (42)') + assert '42' in str(exc_info.value) class TestWhereClause: From b289f0a77c78788abc314cc26ec17ad48e082cc4 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 2 Jul 2026 12:01:35 -0700 Subject: [PATCH 2/6] refactor: remove stale sys.path.insert and move re import to module level Remove hardcoded sys.path.insert(0, '/home/sebtardif/MiniDB') from all test files and main.py. This path points to a non-existent Linux directory and is unnecessary since pytest discovers packages via pyproject.toml. Move 'import re' from inside _match_like() to query.py module level. Signed-off-by: Sebastien Tardif --- main.py | 4 ---- minidb/query.py | 2 +- tests/test_aggregations.py | 4 ---- tests/test_crud.py | 4 ---- tests/test_database.py | 4 ---- tests/test_index.py | 4 ---- tests/test_joins.py | 4 ---- tests/test_performance.py | 3 --- tests/test_persistence.py | 3 --- tests/test_queries.py | 4 ---- 10 files changed, 1 insertion(+), 35 deletions(-) diff --git a/main.py b/main.py index 0e753df..43cd0e6 100644 --- a/main.py +++ b/main.py @@ -12,10 +12,6 @@ - Persistence """ -import sys - -sys.path.insert(0, '/home/sebtardif/MiniDB') - from minidb import MiniDB diff --git a/minidb/query.py b/minidb/query.py index 43264c0..459445d 100644 --- a/minidb/query.py +++ b/minidb/query.py @@ -1,5 +1,6 @@ """Query execution engine for MiniDB.""" +import re from collections import defaultdict from typing import Any @@ -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 diff --git a/tests/test_aggregations.py b/tests/test_aggregations.py index 70b0ed5..0e84185 100644 --- a/tests/test_aggregations.py +++ b/tests/test_aggregations.py @@ -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 diff --git a/tests/test_crud.py b/tests/test_crud.py index 6247a54..0a55bb7 100644 --- a/tests/test_crud.py +++ b/tests/test_crud.py @@ -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 diff --git a/tests/test_database.py b/tests/test_database.py index b6f661d..dcfb1ee 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -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 diff --git a/tests/test_index.py b/tests/test_index.py index 2d189b2..6c6958e 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -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 diff --git a/tests/test_joins.py b/tests/test_joins.py index 56a0dae..6e3ef54 100644 --- a/tests/test_joins.py +++ b/tests/test_joins.py @@ -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 diff --git a/tests/test_performance.py b/tests/test_performance.py index 4f59ca8..1f0375b 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -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 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 27b599c..6c9376e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -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 diff --git a/tests/test_queries.py b/tests/test_queries.py index b7b94bd..0b726d2 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -1,11 +1,7 @@ """Tests for WHERE, ORDER BY, GROUP BY queries.""" -import sys - import pytest -sys.path.insert(0, '/home/sebtardif/MiniDB') - from minidb import Column, ColumnType, MiniDB from minidb.errors import SyntaxError_ From 5f351f825a2fb530204de58fd6ba538175a214a9 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 2 Jul 2026 12:02:41 -0700 Subject: [PATCH 3/6] docs: fix stale test count, clone URL, and LEFT JOIN documentation - Update test count from 29+ to 95+ - Replace placeholder clone URL with actual GitHub URL - Document LEFT JOIN support in Features, SQL Reference, and Limitations - Add Python 3.14 classifier to pyproject.toml - Update test_queries.py description to include parser/type tests Signed-off-by: Sebastien Tardif --- README.md | 16 ++++++++++------ pyproject.toml | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b7d5f30..6d1a1dd 100644 --- a/README.md +++ b/README.md @@ -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 @@ -23,7 +23,7 @@ No installation required! MiniDB uses only Python standard library. ```bash # Just clone and use -git clone +git clone https://github.com/SebTardif/MiniDB.git cd MiniDB python -m pytest tests/ -v # Run tests python main.py # Run demo @@ -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 @@ -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 @@ -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 @@ -233,8 +238,7 @@ 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 diff --git a/pyproject.toml b/pyproject.toml index 43c3d13..f36ec33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] From cd328fbecb86ef6191b9e5ec976fd46018133231 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 2 Jul 2026 12:03:49 -0700 Subject: [PATCH 4/6] ci: add Python 3.14 to test matrix and .mypy_cache to .gitignore - Add Python 3.14 to CI test matrix (already tested locally, classifier added in prior commit) - Add .mypy_cache/ to .gitignore to prevent accidental tracking Signed-off-by: Sebastien Tardif --- .github/workflows/ci.yml | 2 +- .gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f54a520..c681af0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index b06b909..bf3cb29 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build/ coverage.xml htmlcov/ .pytest_cache/ +.mypy_cache/ # IDE .vscode/ From afad8ea8074ef523f56d921d58c0800d895863c0 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 2 Jul 2026 12:05:40 -0700 Subject: [PATCH 5/6] perf: eliminate redundant double-sort in ORDER BY DESC When ORDER BY direction was DESC, _execute_order_by sorted the rows ascending first, then re-sorted descending. This doubled the sort work for every DESC query. Fixed by computing the reverse flag once and passing it to a single sorted() call. Signed-off-by: Sebastien Tardif --- minidb/query.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/minidb/query.py b/minidb/query.py index 459445d..abf8b76 100644 --- a/minidb/query.py +++ b/minidb/query.py @@ -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.""" From c524e889e8f8f61b0c705071eb9de924872426ef Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 2 Jul 2026 12:07:27 -0700 Subject: [PATCH 6/6] docs: add CONTRIBUTING.md with dev setup and pre-commit checks New contributors can now find the complete development workflow: prerequisites, dev tool installation, pre-commit checks matching CI, conventional commit format, and project structure overview. README.md now links to CONTRIBUTING.md. Signed-off-by: Sebastien Tardif --- CONTRIBUTING.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e6b644d --- /dev/null +++ b/CONTRIBUTING.md @@ -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 +``` diff --git a/README.md b/README.md index 6d1a1dd..16284eb 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,9 @@ 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