From 6f6dff28d6a7f60f423142a24505e9346843a1c6 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Sat, 12 Oct 2024 09:53:25 +0100 Subject: [PATCH 1/5] implement user-defined indexes Signed-off-by: Grant Ramsay --- sqliter/model/model.py | 27 ++++++++++++++++++---- sqliter/sqliter.py | 52 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/sqliter/model/model.py b/sqliter/model/model.py index 130c3a8..de67b85 100644 --- a/sqliter/model/model.py +++ b/sqliter/model/model.py @@ -10,7 +10,16 @@ from __future__ import annotations import re -from typing import Any, Optional, TypeVar, Union, cast, get_args, get_origin +from typing import ( + Any, + ClassVar, + Optional, + TypeVar, + Union, + cast, + get_args, + get_origin, +) from pydantic import BaseModel, ConfigDict, Field @@ -41,14 +50,24 @@ class Meta: """Metadata class for configuring database-specific attributes. Attributes: - create_pk (bool): Whether to create a primary key field. - primary_key (str): The name of the primary key field. - table_name (Optional[str]): The name of the database table. + table_name (Optional[str]): The name of the database table. If not + specified, the table name will be inferred from the model class + name and converted to snake_case. + indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of fields + or tuples of fields for which regular (non-unique) indexes + should be created. Indexes improve query performance on these + fields. + unique_indexes (ClassVar[list[Union[str, tuple[str]]]]): A list of + fields or tuples of fields for which unique indexes should be + created. Unique indexes enforce that all values in these fields + are distinct across the table. """ table_name: Optional[str] = ( None # Table name, defaults to class name if not set ) + indexes: ClassVar[list[Union[str, tuple[str]]]] = [] + unique_indexes: ClassVar[list[Union[str, tuple[str]]]] = [] @classmethod def model_validate_partial(cls: type[T], obj: dict[str, Any]) -> T: diff --git a/sqliter/sqliter.py b/sqliter/sqliter.py index 5ce7817..2a3d2d5 100644 --- a/sqliter/sqliter.py +++ b/sqliter/sqliter.py @@ -10,7 +10,7 @@ import logging import sqlite3 -from typing import TYPE_CHECKING, Any, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union from typing_extensions import Self @@ -261,6 +261,56 @@ def create_table( except sqlite3.Error as exc: raise TableCreationError(table_name) from exc + # Create regular indexes + if hasattr(model_class.Meta, "indexes"): + self._create_indexes( + model_class, model_class.Meta.indexes, unique=False + ) + + # Create unique indexes + if hasattr(model_class.Meta, "unique_indexes"): + self._create_indexes( + model_class, model_class.Meta.unique_indexes, unique=True + ) + + def _create_indexes( + self, + model_class: type[BaseDBModel], + indexes: list[Union[str, tuple[str]]], + *, + unique: bool = False, + ) -> None: + """Helper method to create regular or unique indexes. + + Args: + model_class: The model class defining the table. + indexes: List of fields or tuples of fields to create indexes for. + unique: If True, creates UNIQUE indexes; otherwise, creates regular + indexes. + """ + for index in indexes: + # Handle multiple fields in tuple form + if isinstance(index, tuple): + index_name = "_".join(index) + fields = list(index) # Ensure fields is a list of strings + else: + index_name = index + fields = [index] # Wrap single field in a list + + # Add '_unique' postfix to index name for unique indexes + index_postfix = "_unique" if unique else "" + index_type = ( + "UNIQUE" if unique else "" + ) # Add UNIQUE for unique indexes + + create_index_sql = ( + f"CREATE {index_type} INDEX IF NOT EXISTS " + f"idx_{model_class.get_table_name()}" + f"_{index_name}{index_postfix} " + f"ON {model_class.get_table_name()} ({', '.join(fields)})" + ) + self._execute_sql(create_index_sql) + def _execute_sql(self, sql: str) -> None: """Execute an SQL statement. From f61ee24753f64ab72afd7063bf3f7eedf88829db Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Sat, 12 Oct 2024 10:00:25 +0100 Subject: [PATCH 2/5] handle index errors (invalid names) Signed-off-by: Grant Ramsay --- sqliter/exceptions.py | 28 ++++++++++++++++++++++++++++ sqliter/sqliter.py | 32 +++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/sqliter/exceptions.py b/sqliter/exceptions.py index f248a94..8ba3f29 100644 --- a/sqliter/exceptions.py +++ b/sqliter/exceptions.py @@ -145,3 +145,31 @@ class SqlExecutionError(SqliterError): """Raised when an SQL execution fails.""" message_template = "Failed to execute SQL: '{}'" + + +class InvalidIndexError(SqliterError): + """Exception raised when an invalid index field is specified. + + This error is triggered if one or more fields specified for an index + do not exist in the model's fields. + + Attributes: + invalid_fields (list[str]): The list of fields that were invalid. + model_class (str): The name of the model where the error occurred. + """ + + message_template = ( + "Invalid fields for indexing in model '{model_class}': {invalid_fields}" + ) + + def __init__(self, invalid_fields: list[str], model_class: str) -> None: + """Initialize the exception with invalid fields and model name. + + Args: + invalid_fields (list[str]): List of the invalid fields. + model_class (str): Name of the model class where the error occurred. + """ + formatted_message = self.message_template.format( + model_class=model_class, invalid_fields=", ".join(invalid_fields) + ) + super().__init__(formatted_message) diff --git a/sqliter/sqliter.py b/sqliter/sqliter.py index 2a3d2d5..14cc19f 100644 --- a/sqliter/sqliter.py +++ b/sqliter/sqliter.py @@ -16,6 +16,7 @@ from sqliter.exceptions import ( DatabaseConnectionError, + InvalidIndexError, RecordDeletionError, RecordFetchError, RecordInsertionError, @@ -287,21 +288,30 @@ def _create_indexes( indexes: List of fields or tuples of fields to create indexes for. unique: If True, creates UNIQUE indexes; otherwise, creates regular indexes. + + Raises: + InvalidIndexError: If any fields specified for indexing do not exist + in the model. """ + valid_fields = set( + model_class.model_fields.keys() + ) # Get valid fields from the model + for index in indexes: # Handle multiple fields in tuple form - if isinstance(index, tuple): - index_name = "_".join(index) - fields = list(index) # Ensure fields is a list of strings - else: - index_name = index - fields = [index] # Wrap single field in a list - - # Add '_unique' postfix to index name for unique indexes + fields = list(index) if isinstance(index, tuple) else [index] + + # Check if all fields exist in the model + invalid_fields = [ + field for field in fields if field not in valid_fields + ] + if invalid_fields: + raise InvalidIndexError(invalid_fields, model_class.__name__) + + # Build the SQL string + index_name = "_".join(fields) index_postfix = "_unique" if unique else "" - index_type = ( - "UNIQUE" if unique else "" - ) # Add UNIQUE for unique indexes + index_type = "UNIQUE" if unique else "" create_index_sql = ( f"CREATE {index_type} INDEX IF NOT EXISTS " From 26f1c809dd21aa5f03f2529e52540afd1d4a587d Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Sat, 12 Oct 2024 11:24:18 +0100 Subject: [PATCH 3/5] add index tests and fix some errors Signed-off-by: Grant Ramsay --- sqliter/exceptions.py | 19 ++-- sqliter/sqliter.py | 4 +- tests/test_indexes.py | 231 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 15 deletions(-) create mode 100644 tests/test_indexes.py diff --git a/sqliter/exceptions.py b/sqliter/exceptions.py index 8ba3f29..c7dafe4 100644 --- a/sqliter/exceptions.py +++ b/sqliter/exceptions.py @@ -158,18 +158,11 @@ class InvalidIndexError(SqliterError): model_class (str): The name of the model where the error occurred. """ - message_template = ( - "Invalid fields for indexing in model '{model_class}': {invalid_fields}" - ) + message_template = "Invalid fields for indexing in model '{}': {}" def __init__(self, invalid_fields: list[str], model_class: str) -> None: - """Initialize the exception with invalid fields and model name. - - Args: - invalid_fields (list[str]): List of the invalid fields. - model_class (str): Name of the model class where the error occurred. - """ - formatted_message = self.message_template.format( - model_class=model_class, invalid_fields=", ".join(invalid_fields) - ) - super().__init__(formatted_message) + """Tidy up the error message by joining the invalid fields.""" + # Join invalid fields into a comma-separated string + invalid_fields_str = ", ".join(invalid_fields) + # Pass the formatted message to the parent class + super().__init__(model_class, invalid_fields_str) diff --git a/sqliter/sqliter.py b/sqliter/sqliter.py index 14cc19f..18f43b9 100644 --- a/sqliter/sqliter.py +++ b/sqliter/sqliter.py @@ -311,10 +311,10 @@ def _create_indexes( # Build the SQL string index_name = "_".join(fields) index_postfix = "_unique" if unique else "" - index_type = "UNIQUE" if unique else "" + index_type = " UNIQUE " if unique else " " create_index_sql = ( - f"CREATE {index_type} INDEX IF NOT EXISTS " + f"CREATE{index_type}INDEX IF NOT EXISTS " f"idx_{model_class.get_table_name()}" f"_{index_name}{index_postfix} " f"ON {model_class.get_table_name()} ({', '.join(fields)})" diff --git a/tests/test_indexes.py b/tests/test_indexes.py new file mode 100644 index 0000000..91ba960 --- /dev/null +++ b/tests/test_indexes.py @@ -0,0 +1,231 @@ +"""Test suite for index creation in the database.""" + +from typing import ClassVar + +import pytest +from pytest_mock import MockerFixture + +from sqliter.exceptions import InvalidIndexError +from sqliter.model import BaseDBModel +from sqliter.sqliter import SqliterDB + + +def get_index_names(db: SqliterDB) -> list[str]: + """Helper function to fetch index names from sqlite_master.""" + conn = db.conn + if conn: + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='index'") + return [row[0] for row in cursor.fetchall()] + return [] + + +class TestIndexes: + """Test cases for index creation in the database.""" + + def test_regular_index_creation(self, mocker: MockerFixture) -> None: + """Test that regular indexes are created for valid fields.""" + mock_execute = mocker.patch.object(SqliterDB, "_execute_sql") + + db = SqliterDB(":memory:") + + # Define a test model with a regular index on 'email' + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[str]] = ["email"] # Regular index + + # Create the table + db.create_table(UserModel) + + # Assert the correct SQL for the index was executed + expected_sql = ( + "CREATE INDEX IF NOT EXISTS idx_users_email ON users (email)" + ) + mock_execute.assert_any_call(expected_sql) + + def test_unique_index_creation(self, mocker: MockerFixture) -> None: + """Test that unique indexes are created for valid fields.""" + mock_execute = mocker.patch.object(SqliterDB, "_execute_sql") + + db = SqliterDB(":memory:") + + # Define a test model with a unique index on 'email' + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + unique_indexes: ClassVar[list[str]] = ["email"] # Unique index + + # Create the table + db.create_table(UserModel) + + # Assert the correct SQL for the unique index was executed + expected_sql = ( + "CREATE UNIQUE INDEX IF NOT EXISTS " + "idx_users_email_unique ON users (email)" + ) + mock_execute.assert_any_call(expected_sql) + + def test_composite_index_creation(self, mocker: MockerFixture) -> None: + """Test composite index creation for valid fields.""" + mock_execute = mocker.patch.object(SqliterDB, "_execute_sql") + + db = SqliterDB(":memory:") + + # Define a test model with a composite index on 'customer_id' and + # 'order_id' + class OrderModel(BaseDBModel): + order_id: str + customer_id: str + + class Meta: + table_name = "orders" + indexes: ClassVar[list[tuple[str, str]]] = [ + ("customer_id", "order_id") + ] # Composite index + + # Create the table + db.create_table(OrderModel) + + # Assert the correct SQL for the composite index was executed + expected_sql = ( + "CREATE INDEX IF NOT EXISTS idx_orders_customer_id_order_id " + "ON orders (customer_id, order_id)" + ) + mock_execute.assert_any_call(expected_sql) + + def test_invalid_index_raises_error(self) -> None: + """Test that an invalid index raises an InvalidIndexError.""" + db = SqliterDB(":memory:") + + # Define a test model with an invalid index field + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[str]] = [ + "non_existent_field" + ] # Invalid field + + # Assert an InvalidIndexError is raised + with pytest.raises(InvalidIndexError) as exc_info: + db.create_table(UserModel) + + # Ensure the error message contains the invalid field and model class + assert "Invalid fields for indexing in model 'UserModel'" in str( + exc_info.value + ) + + def test_actual_regular_index_creation(self) -> None: + """Test that the regular index is actually created in the database.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[str]] = ["email"] # Regular index + + # Create the table and index + db.create_table(UserModel) + + # Use helper to fetch index names + index_names: list[str] = get_index_names(db) + + assert "idx_users_email" in index_names + + def test_actual_unique_index_creation(self) -> None: + """Test that the unique index is actually created in the database.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + unique_indexes: ClassVar[list[str]] = ["email"] # Unique index + + # Create the table and unique index + db.create_table(UserModel) + + # Use helper to fetch index names + index_names: list[str] = get_index_names(db) + + assert "idx_users_email_unique" in index_names + + def test_no_index_creation_with_empty_indexes(self) -> None: + """Test no index created when indexes and unique_indexes are empty.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[str]] = [] + unique_indexes: ClassVar[list[str]] = [] + + # Create the table with no indexes + db.create_table(UserModel) + + # Use helper to fetch index names + index_names: list[str] = get_index_names(db) + + assert len(index_names) == 0 + + def test_invalid_index_with_bad_tuple(self) -> None: + """Test InvalidIndexError is raised when tuple has invalid fields.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + name: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[tuple[str, str]]] = [ + ("email", "non_existent_field") + ] # Invalid tuple + + # Assert that InvalidIndexError is raised for the bad tuple + with pytest.raises(InvalidIndexError) as exc_info: + db.create_table(UserModel) + + error_message: str = str(exc_info.value) + assert "non_existent_field" in error_message + + def test_good_index_followed_by_bad_index(self) -> None: + """Test a good index followed by bad index raises InvalidIndexError.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + name: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[str]] = [ + "email", # Good index + "non_existent_field", # Bad index + ] + + # Assert that InvalidIndexError is raised for the bad index + with pytest.raises(InvalidIndexError) as exc_info: + db.create_table(UserModel) + + error_message: str = str(exc_info.value) + assert "non_existent_field" in error_message From baf17ac1a1ab0afe8f9b96e345ed460563ca3b3a Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Sat, 12 Oct 2024 11:42:21 +0100 Subject: [PATCH 4/5] add more test cases Signed-off-by: Grant Ramsay --- tests/test_indexes.py | 105 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/tests/test_indexes.py b/tests/test_indexes.py index 91ba960..0d082bd 100644 --- a/tests/test_indexes.py +++ b/tests/test_indexes.py @@ -5,7 +5,7 @@ import pytest from pytest_mock import MockerFixture -from sqliter.exceptions import InvalidIndexError +from sqliter.exceptions import InvalidIndexError, TableCreationError from sqliter.model import BaseDBModel from sqliter.sqliter import SqliterDB @@ -229,3 +229,106 @@ class Meta: error_message: str = str(exc_info.value) assert "non_existent_field" in error_message + + def test_multiple_valid_composite_indexes(self) -> None: + """Test that multiple valid composite indexes are created.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + name: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[tuple[str, str]]] = [ + ("email", "name"), # Valid composite index + ("slug", "email"), # Another valid composite index + ] + + db.create_table(UserModel) + + index_names: list[str] = get_index_names(db) + assert "idx_users_email_name" in index_names + assert "idx_users_slug_email" in index_names + + def test_index_with_empty_field_in_tuple(self) -> None: + """Test that an index with an empty field in a tuple raises an error.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[tuple[str, str]]] = [ + ("email", ""), # Invalid composite index + ] + + with pytest.raises(InvalidIndexError) as exc_info: + db.create_table(UserModel) + + error_message: str = str(exc_info.value) + assert "Invalid fields" in error_message + + def test_duplicate_index_fields(self) -> None: + """Test that duplicate index fields are handled properly.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[str]] = [ + "email", # First occurrence + "email", # Duplicate + ] + + db.create_table(UserModel) + + # Check that only one index was created + index_names: list[str] = get_index_names(db) + assert index_names.count("idx_users_email") == 1 + + def test_index_with_reserved_keyword_as_field_name(self) -> None: + """Test that fields using reserved SQL keywords raise an error.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + select: str # 'select' is a reserved SQL keyword + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[str]] = [ + "select" + ] # Invalid due to keyword + + with pytest.raises(TableCreationError) as exc_info: + db.create_table(UserModel) + + error_message: str = str(exc_info.value) + assert "select" in error_message + + def test_mixed_valid_and_invalid_fields_in_composite_index(self) -> None: + """Test an index with both valid and invalid fields raises an error.""" + db = SqliterDB(":memory:") + + class UserModel(BaseDBModel): + slug: str + email: str + + class Meta: + table_name = "users" + indexes: ClassVar[list[tuple[str, str]]] = [ + ("email", "invalid_field") # Mixed valid and invalid + ] + + with pytest.raises(InvalidIndexError) as exc_info: + db.create_table(UserModel) + + error_message: str = str(exc_info.value) + assert "invalid_field" in error_message From 61b44efb9bfd8d62103794400d10149fedef61a8 Mon Sep 17 00:00:00 2001 From: Grant Ramsay Date: Sat, 12 Oct 2024 12:00:06 +0100 Subject: [PATCH 5/5] add indexes info to the docs Signed-off-by: Grant Ramsay --- README.md | 16 ++++++++------ TODO.md | 1 - docs/guide/exceptions.md | 28 ++++++++++++++---------- docs/guide/models.md | 47 ++++++++++++++++++++++++++++++++++++++++ docs/index.md | 2 ++ docs/installation.md | 16 +++++++------- docs/quickstart.md | 4 ---- 7 files changed, 82 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index b09e4c4..336928d 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ Website](https://sqliter.grantramsay.dev) ## Features - Table creation based on Pydantic models +- Automatic primary key generation +- User defined indexes on any field - CRUD operations (Create, Read, Update, Delete) - Chained Query building with filtering, ordering, and pagination - Transaction support @@ -70,16 +72,16 @@ virtual environments (`uv` is used for developing this project and in the CI): uv add sqliter-py ``` -With `pip`: +With `Poetry`: ```bash -pip install sqliter-py +poetry add sqliter-py ``` -Or with `Poetry`: +Or with `pip`: ```bash -poetry add sqliter-py +pip install sqliter-py ``` ### Optional Dependencies @@ -88,9 +90,9 @@ Currently by default, the only external dependency is Pydantic. However, there are some optional dependencies that can be installed to enable additional features: -- `inflect`: For pluralizing table names (if not specified). This just offers a - more-advanced pluralization than the default method used. In most cases you - will not need this. +- `inflect`: For pluralizing the auto-generated table names (if not explicitly + set in the Model) This just offers a more-advanced pluralization than the + default method used. In most cases you will not need this. See [Installing Optional Dependencies](https://sqliter.grantramsay.dev/installation#optional-dependencies) diff --git a/TODO.md b/TODO.md index 8336a13..b9ed076 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,6 @@ 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. -- allow adding multiple indexes to each table as well as the primary key. - allow adding foreign keys and relationships to each table. - add a migration system to allow updating the database schema without losing data. diff --git a/docs/guide/exceptions.md b/docs/guide/exceptions.md index 4dfe687..dd2caa2 100644 --- a/docs/guide/exceptions.md +++ b/docs/guide/exceptions.md @@ -10,51 +10,55 @@ class, `SqliterError`, to ensure consistency across error messages and behavior. - **Message**: "An error occurred in the SQLiter package." - **`DatabaseConnectionError`**: - - Raised when the SQLite database connection fails. + - **Raised** when the SQLite database connection fails. - **Message**: "Failed to connect to the database: '{}'." - **`InvalidOffsetError`**: - - Raised when an invalid offset value (0 or negative) is used in queries. + - **Raised** when an invalid offset value (0 or negative) is used in queries. - **Message**: "Invalid offset value: '{}'. Offset must be a positive integer." - **`InvalidOrderError`**: - - Raised when an invalid order value is used in queries, such as a + - **Raised** when an invalid order value is used in queries, such as a non-existent field or an incorrect sorting direction. - **Message**: "Invalid order value - '{}'" - **`TableCreationError`**: - - Raised when a table cannot be created in the database. + - **Raised** when a table cannot be created in the database. - **Message**: "Failed to create the table: '{}'." - **`RecordInsertionError`**: - - Raised when an error occurs during record insertion. + - **Raised** when an error occurs during record insertion. - **Message**: "Failed to insert record into table: '{}'." - **`RecordUpdateError`**: - - Raised when an error occurs during record update. + - **Raised** when an error occurs during record update. - **Message**: "Failed to update record in table: '{}'." - **`RecordNotFoundError`**: - - Raised when a record with the specified primary key is not found. + - **Raised** when a record with the specified primary key is not found. - **Message**: "Failed to find a record for key '{}'". - **`RecordFetchError`**: - - Raised when an error occurs while fetching records from the database. + - **Raised** when an error occurs while fetching records from the database. - **Message**: "Failed to fetch record from table: '{}'." - **`RecordDeletionError`**: - - Raised when an error occurs during record deletion. + - **Raised** when an error occurs during record deletion. - **Message**: "Failed to delete record from table: '{}'." - **`InvalidFilterError`**: - - Raised when an invalid filter field is used in a query. + - **Raised** when an invalid filter field is used in a query. - **Message**: "Failed to apply filter: invalid field '{}'". - **`TableDeletionError`**: - - Raised when a table cannot be deleted from the database. + - **Raised** when a table cannot be deleted from the database. - **Message**: "Failed to delete the table: '{}'." - **SqlExecutionError** - - Raised when an error occurs during SQL query execution. + - **Raised** when an error occurs during SQL query execution. - **Message**: "Failed to execute SQL: '{}'." + +- **InvalidIndexError** + - **Raised** when an invalid index is specified for a model. + - **Message**: "Invalid fields for indexing in model '{}': {}" diff --git a/docs/guide/models.md b/docs/guide/models.md index e4c4145..096f8dd 100644 --- a/docs/guide/models.md +++ b/docs/guide/models.md @@ -29,6 +29,53 @@ the table. > - The Model **automatically** creates an **auto-incrementing integer primary > key** for each table called `pk`, you do not need to define it yourself. +### Adding Indexes + +You can add indexes to your table by specifying the `indexes` attribute in the +`Meta` class. This should be a list of strings, each string being the name of an +existing field in the model that should be indexed. + +```python +from sqliter.model import BaseDBModel + +class User(BaseDBModel): + name: str + age: int + email: str + + class Meta: + indexes = ["name", "email"] +``` + +This is in addition to the primary key index (`pk`) that is automatically +created. + +### Adding Unique Indexes + +You can add unique indexes to your table by specifying the `unique_indexes` +attribute in the `Meta` class. This should be a list of strings, each string +being the name of an existing field in the model that should be indexed. + +```python +from sqliter.model import BaseDBModel + +class User(BaseDBModel): + name: str + age: int + email: str + + class Meta: + unique_indexes = ["email"] +``` + +These will ensure that all values in this field are unique. This is in addition +to the primary key index (`pk`) that is automatically created. + +> [!TIP] +> +> You can specify both `indexes` and `unique_indexes` in the `Meta` class if you +> need to. + ### Custom Table Name By default, the table name will be the same as the model name, converted to diff --git a/docs/index.md b/docs/index.md index c45e465..7b85bb4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,6 +38,8 @@ database-like format without needing to learn SQL or use a full ORM. ## Features - Table creation based on Pydantic models +- Automatic primary key generation +- User defined indexes on any field - CRUD operations (Create, Read, Update, Delete) - Chained Query building with filtering, ordering, and pagination - Transaction support diff --git a/docs/installation.md b/docs/installation.md index 15badfd..88faac4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,16 +10,16 @@ virtual environments (`uv` is used for developing this project and in the CI): uv add sqliter-py ``` -With `pip`: +With `Poetry`: ```bash -pip install sqliter-py +poetry add sqliter-py ``` -Or with `Poetry`: +Or with `pip`: ```bash -poetry add sqliter-py +pip install sqliter-py ``` ## Optional Dependencies @@ -38,14 +38,14 @@ These can be installed using `uv`: uv add 'sqliter-py[extras]' ``` -Or with `pip`: +With `Poetry`: ```bash -pip install 'sqliter-py[extras]' +poetry add 'sqliter-py[extras]' ``` -Or with `Poetry`: +Or with `pip`: ```bash -poetry add 'sqliter-py[extras]' +pip install 'sqliter-py[extras]' ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 21f63b9..931f965 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,10 +14,6 @@ class User(BaseDBModel): age: int admin: Optional[bool] = False - class Meta: - create_pk = False - primary_key = "name" - # Create a database connection db = SqliterDB("example.db")