Skip to content

Commit 273ee95

Browse files
authored
Merge pull request #48 from seapagan/add-properties
2 parents 2024032 + 3d9acd3 commit 273ee95

File tree

6 files changed

+312
-3
lines changed

6 files changed

+312
-3
lines changed

TODO.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
- add (optional) `created_at` and `updated_at` fields to the BaseDBModel class
66
which will be automatically updated when a record is created or updated.
7-
- add attributes to the BaseDBModel to read the table-name, file-name, is-memory
8-
etc.
97
- add an 'execute' method to the main class to allow executing arbitrary SQL
108
queries which can be chained to the 'find_first' etc methods or just used
119
directly.

docs/guide/connecting.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,9 @@ db = SqliterDB("your_database.db", reset=True)
6565
6666
This will effectively drop all user tables from the database. The file itself is
6767
not deleted, only the tables are dropped.
68+
69+
## Database Properties
70+
71+
The `SqliterDB` class provides several properties to access information about
72+
the database instance once it has been created. See the
73+
[Properties](properties.md) page (next) for more details.

docs/guide/properties.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
2+
# SqliterDB Properties
3+
4+
## Overview
5+
6+
The `SqliterDB` class includes several useful **read-only** properties that
7+
provide insight into the current state of the database. These properties allow
8+
users to easily query key database attributes, such as the filename, whether the
9+
database is in memory, auto-commit status, and the list of tables.
10+
11+
### Properties
12+
13+
1. **`filename`**
14+
Returns the filename of the database, or `None` if the database is in-memory.
15+
16+
**Usage Example**:
17+
18+
```python
19+
db = SqliterDB(db_filename="test.db")
20+
print(db.filename) # Output: 'test.db'
21+
```
22+
23+
2. **`is_memory`**
24+
Returns `True` if the database is in-memory, otherwise `False`.
25+
26+
**Usage Example**:
27+
28+
```python
29+
db = SqliterDB(memory=True)
30+
print(db.is_memory) # Output: True
31+
```
32+
33+
3. **`is_autocommit`**
34+
Returns `True` if the database is in auto-commit mode, otherwise `False`.
35+
36+
**Usage Example**:
37+
38+
```python
39+
db = SqliterDB(auto_commit=True)
40+
print(db.is_autocommit) # Output: True
41+
```
42+
43+
4. **`table_names`**
44+
Returns a list of all user-defined table names in the database. The property temporarily reconnects if the connection is closed.
45+
46+
**Usage Example**:
47+
48+
```python
49+
db = SqliterDB(memory=True)
50+
db.create_table(User) # Assume 'User' is a predefined model
51+
print(db.table_names) # Output: ['user']
52+
```
53+
54+
## Property Details
55+
56+
### `filename`
57+
58+
This property allows users to retrieve the current database filename. For in-memory databases, this property returns `None`, as no filename is associated with an in-memory database.
59+
60+
- **Type**: `Optional[str]`
61+
- **Returns**: The database filename or `None` if in memory.
62+
63+
### `is_memory`
64+
65+
This property indicates whether the database is in memory. It simplifies the check for memory-based databases, returning `True` for in-memory and `False` otherwise.
66+
67+
- **Type**: `bool`
68+
- **Returns**: `True` if the database is in memory, otherwise `False`.
69+
70+
### `is_autocommit`
71+
72+
This property returns whether the database is in auto-commit mode. If `auto_commit` is enabled, every operation is automatically committed without requiring an explicit `commit()` call.
73+
74+
- **Type**: `bool`
75+
- **Returns**: `True` if auto-commit mode is enabled, otherwise `False`.
76+
77+
### `table_names`
78+
79+
This property retrieves a list of user-defined table names from the database. It does not include system tables (`sqlite_`). If the database connection is closed, this property will temporarily reconnect to query the table names and close the connection afterward.
80+
81+
- **Type**: `list[str]`
82+
- **Returns**: A list of user-defined table names in the database.
83+
- **Raises**: `DatabaseConnectionError` if the database connection fails to re-establish.
84+
85+
## Example
86+
87+
Here's a complete example demonstrating the use of the new properties:
88+
89+
```python
90+
from sqliter import SqliterDB
91+
from sqliter.model import BaseDBModel
92+
93+
# Define a simple model
94+
class User(BaseDBModel):
95+
id: int
96+
name: str
97+
98+
# Create an in-memory database
99+
db = SqliterDB(memory=True)
100+
db.create_table(User)
101+
102+
# Access properties
103+
print(db.filename) # Output: None
104+
print(db.is_memory) # Output: True
105+
print(db.is_autocommit) # Output: True (this is the default)
106+
print(db.table_names) # Output: ['user']
107+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ nav:
7272
- Overview: guide/guide.md
7373
- Models: guide/models.md
7474
- Connect to a Database: guide/connecting.md
75+
- Properties: guide/properties.md
7576
- Table Operations: guide/tables.md
7677
- Data Operations: guide/data-ops.md
7778
- Transactions: guide/transactions.md

sqliter/sqliter.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class SqliterDB:
5151
logger (Optional[logging.Logger]): Custom logger for debug output.
5252
"""
5353

54+
MEMORY_DB = ":memory:"
55+
5456
def __init__(
5557
self,
5658
db_filename: Optional[str] = None,
@@ -76,7 +78,7 @@ def __init__(
7678
ValueError: If no filename is provided for a non-memory database.
7779
"""
7880
if memory:
79-
self.db_filename = ":memory:"
81+
self.db_filename = self.MEMORY_DB
8082
elif db_filename:
8183
self.db_filename = db_filename
8284
else:
@@ -99,6 +101,54 @@ def __init__(
99101
if self.reset:
100102
self._reset_database()
101103

104+
@property
105+
def filename(self) -> Optional[str]:
106+
"""Returns the filename of the current database or None if in-memory."""
107+
return None if self.db_filename == self.MEMORY_DB else self.db_filename
108+
109+
@property
110+
def is_memory(self) -> bool:
111+
"""Returns True if the database is in-memory."""
112+
return self.db_filename == self.MEMORY_DB
113+
114+
@property
115+
def is_autocommit(self) -> bool:
116+
"""Returns True if auto-commit is enabled."""
117+
return self.auto_commit
118+
119+
@property
120+
def is_connected(self) -> bool:
121+
"""Returns True if the database is connected, False otherwise."""
122+
return self.conn is not None
123+
124+
@property
125+
def table_names(self) -> list[str]:
126+
"""Returns a list of all table names in the database.
127+
128+
Temporarily connects to the database if not connected and restores
129+
the connection state afterward.
130+
"""
131+
was_connected = self.is_connected
132+
if not was_connected:
133+
self.connect()
134+
135+
if self.conn is None:
136+
err_msg = "Failed to establish a database connection."
137+
raise DatabaseConnectionError(err_msg)
138+
139+
cursor = self.conn.cursor()
140+
cursor.execute(
141+
"SELECT name FROM sqlite_master WHERE type='table' "
142+
"AND name NOT LIKE 'sqlite_%';"
143+
)
144+
tables = [row[0] for row in cursor.fetchall()]
145+
146+
# Restore the connection state
147+
if not was_connected:
148+
self.close()
149+
150+
return tables
151+
102152
def _reset_database(self) -> None:
103153
"""Drop all user-created tables in the database."""
104154
with self.connect() as conn:

tests/test_properties.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Test the read-only properties in the SqliterDB class."""
2+
3+
import tempfile
4+
5+
import pytest
6+
7+
from sqliter.exceptions import DatabaseConnectionError
8+
from sqliter.model.model import BaseDBModel
9+
from sqliter.sqliter import SqliterDB
10+
11+
12+
class TestSqliterDBProperties:
13+
"""Test suite for the read-only properties in the SqliterDB class."""
14+
15+
def test_filename_property_memory_db(self) -> None:
16+
"""Test the 'filename' property for an in-memory database."""
17+
db = SqliterDB(memory=True)
18+
assert db.filename is None, "Expected None for in-memory database"
19+
20+
def test_filename_property_file_db(self) -> None:
21+
"""Test the 'filename' property for a file-based database."""
22+
db = SqliterDB(db_filename="test.db")
23+
assert db.filename == "test.db", "Expected 'test.db' as filename"
24+
25+
def test_is_memory_property_true(self) -> None:
26+
"""Test the 'is_memory' property returns True for in-memory database."""
27+
db = SqliterDB(memory=True)
28+
assert db.is_memory is True, "Expected True for in-memory database"
29+
30+
def test_is_memory_property_false(self) -> None:
31+
"""Test 'is_memory' property returns False for file-based database."""
32+
db = SqliterDB(db_filename="test.db")
33+
assert db.is_memory is False, "Expected False for file-based database"
34+
35+
def test_is_autocommit_property_true(self) -> None:
36+
"""Test 'is_autocommit' prop returns True when auto-commit enabled."""
37+
db = SqliterDB(memory=True, auto_commit=True)
38+
assert db.is_autocommit is True, "Expected True for auto-commit enabled"
39+
40+
def test_is_autocommit_property_false(self) -> None:
41+
"""Test 'is_autocommit' prop returns False when auto-commit disabled."""
42+
db = SqliterDB(memory=True, auto_commit=False)
43+
assert (
44+
db.is_autocommit is False
45+
), "Expected False for auto-commit disabled"
46+
47+
def test_is_connected_property_when_connected(self) -> None:
48+
"""Test the 'is_connected' property when the database is connected."""
49+
db = SqliterDB(memory=True)
50+
with db.connect():
51+
assert db.is_connected is True, "Expected True when connected"
52+
53+
def test_is_connected_property_when_disconnected(self) -> None:
54+
"""Test 'is_connected' property when the database is disconnected."""
55+
db = SqliterDB(memory=True)
56+
assert db.is_connected is False, "Expected False when not connected"
57+
58+
def test_table_names_property(self) -> None:
59+
"""Test the 'table_names' property returns correct tables."""
60+
61+
# Define a simple model for the test
62+
class TestTableModel(BaseDBModel):
63+
id: int
64+
65+
class Meta:
66+
table_name = "test_table"
67+
68+
# Create the database without using the context manager
69+
db = SqliterDB(memory=True)
70+
db.create_table(TestTableModel) # ORM-based table creation
71+
72+
# Verify that the table exists while the connection is still open
73+
table_names = db.table_names
74+
assert (
75+
"test_table" in table_names
76+
), f"Expected 'test_table', got {table_names}"
77+
78+
# Explicitly close the connection afterwards
79+
db.close()
80+
81+
def test_table_names_property_when_disconnected(self) -> None:
82+
"""Test the 'table_names' property with no active connection."""
83+
84+
# Define a simple model for the test
85+
class AnotherTableModel(BaseDBModel):
86+
id: int
87+
88+
class Meta:
89+
table_name = "another_table"
90+
91+
# Create the database without the context manager
92+
db = SqliterDB(memory=True)
93+
db.create_table(AnotherTableModel) # ORM-based table creation
94+
95+
# Check the table names while the connection is still open
96+
table_names = db.table_names
97+
assert (
98+
"another_table" in table_names
99+
), f"Expected 'another_table', got {table_names}"
100+
101+
# Close the connection explicitly after the check
102+
db.close()
103+
104+
def test_table_names_property_no_connection_error(self) -> None:
105+
"""Test the 'table_names' property reconnects after disconnection.
106+
107+
This test uses a real temp database file since sqlite3 seems to bypass
108+
the 'pyfakefs' filesystem.
109+
"""
110+
with tempfile.NamedTemporaryFile(suffix=".sqlite") as temp_db:
111+
db_filename = temp_db.name
112+
113+
# Define a simple model for the test
114+
class TestTableModel(BaseDBModel):
115+
id: int
116+
117+
class Meta:
118+
table_name = "test_table"
119+
120+
# Create the database using the temporary file
121+
db = SqliterDB(db_filename=db_filename)
122+
db.create_table(TestTableModel)
123+
124+
# Close the connection
125+
db.close()
126+
127+
# Ensure that accessing table_names does NOT raise an error Since
128+
# it's file-based, the table should still exist after reconnecting
129+
table_names = db.table_names
130+
assert (
131+
"test_table" in table_names
132+
), f"Expected 'test_table', got {table_names}"
133+
134+
def test_table_names_connection_failure(self, mocker) -> None:
135+
"""Test 'table_names' raises exception if the connection fails."""
136+
# Create an instance of the database
137+
db = SqliterDB(memory=True)
138+
139+
# Mock the connect method to simulate a failed connection
140+
mocker.patch.object(db, "connect", return_value=None)
141+
142+
# Close any existing connection to ensure db.conn is None
143+
db.close()
144+
145+
# Attempt to access table_names and expect DatabaseConnectionError
146+
with pytest.raises(DatabaseConnectionError):
147+
_ = db.table_names

0 commit comments

Comments
 (0)