ic-python-db 0.12.0__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {ic_python_db-0.12.0/ic_python_db.egg-info → ic_python_db-0.13.0}/PKG-INFO +30 -7
  2. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/README.md +29 -6
  3. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/__init__.py +5 -1
  4. ic_python_db-0.13.0/ic_python_db/constants.py +11 -0
  5. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/db_engine.py +338 -95
  6. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/entity.py +133 -36
  7. ic_python_db-0.13.0/ic_python_db/errors.py +10 -0
  8. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/properties.py +158 -59
  9. {ic_python_db-0.12.0 → ic_python_db-0.13.0/ic_python_db.egg-info}/PKG-INFO +30 -7
  10. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db.egg-info/SOURCES.txt +1 -0
  11. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/pyproject.toml +1 -1
  12. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/setup.py +1 -1
  13. ic_python_db-0.12.0/ic_python_db/constants.py +0 -6
  14. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/LICENSE +0 -0
  15. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/MANIFEST.in +0 -0
  16. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/_cdk.py +0 -0
  17. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/context.py +0 -0
  18. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/hooks.py +0 -0
  19. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/mixins.py +0 -0
  20. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/py.typed +0 -0
  21. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/schema.py +0 -0
  22. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/storage.py +0 -0
  23. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db/system_time.py +0 -0
  24. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db.egg-info/dependency_links.txt +0 -0
  25. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/ic_python_db.egg-info/top_level.txt +0 -0
  26. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/requirements-dev.txt +0 -0
  27. {ic_python_db-0.12.0 → ic_python_db-0.13.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ic-python-db
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: A lightweight key-value database written in Python, intended for use on the Internet Computer (IC)
5
5
  Home-page: https://github.com/smart-social-contracts/ic-python-db
6
6
  Author: Smart Social Contracts
@@ -60,7 +60,7 @@ A lightweight key-value database with entity relationships and audit logging cap
60
60
  ## Features
61
61
 
62
62
  - **Persistent Storage**: Works with StableBTreeMap stable structure for persistent storage on your canister's stable memory so your data persists automatically across canister upgrades.
63
- - **Range-based paging**: `load_some`, `find`, `instances` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id.
63
+ - **Range-based paging**: `page`, `load_some` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id. `instances()` and `find()` load a whole type: they are deprecated and raise `TooLargeError` past 1,000 entities (use `count()`, `find_by()` or `page()`).
64
64
  - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
65
65
  - **Schema Versioning & Upgrade Safety**: Automatic schema introspection, compatibility checking, and auto-migration for safe changes. Breaking changes without a `migrate()` method are rejected, preventing data corruption on canister upgrades.
66
66
  - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
@@ -163,11 +163,12 @@ Then use the defined entities to store objects:
163
163
 
164
164
  assert Person.count() == 3
165
165
  assert Person.max_id() == 4
166
- assert Person.instances() == [john, alice, eva]
167
166
 
168
- # Cursor-based pagination
169
- assert Person.load_some(0, 2) == [john, alice]
170
- assert Person.load_some(2, 2) == [eva]
167
+ # Cursor-based pagination: (page, next_from_id), None on the last page
168
+ page, cursor = Person.page(1, 2)
169
+ assert page == [john, alice]
170
+ page, cursor = Person.page(cursor, 2)
171
+ assert page == [eva] and cursor is None
171
172
 
172
173
  # Retrieve database contents in JSON format
173
174
  print(Database.get_instance().dump_json(pretty=True))
@@ -229,7 +230,29 @@ while cursor is not None:
229
230
 
230
231
  The rebuild is idempotent. The `indexed` flag is not part of the schema descriptor, so toggling it never triggers upgrade-compatibility errors.
231
232
 
232
- Note: each distinct value's ID list is stored in a single storage entry, so extremely hot values are bounded by the backing store's `max_value_size` — the same constraint as relationship reverse indexes.
233
+ Each matching id is its own storage entry, so a value can match any number of entities without reaching the backing store's `max_value_size`.
234
+
235
+ ## Large Relations
236
+
237
+ `OneToMany` and `ManyToMany` getters return a `RelationView`, not a list. Questions about the relation and single pages never load every member, so they stay cheap however large it grows:
238
+
239
+ ```python
240
+ if section.stands: ... # one read
241
+ len(section.stands) # one read
242
+ stand in section.stands # two reads
243
+ stands, cursor = section.stands.page(1, 50) # one range read + 50 loads
244
+ group.members.add(user) # index writes only
245
+ ```
246
+
247
+ Iterating, indexing and comparing still work, but they load every member, so past `constants.MAX_UNPAGED` (1,000) they raise `TooLargeError` instead of running into the IC instruction limit. Walk a large relation with `page()`, across calls if needed:
248
+
249
+ ```python
250
+ cursor = 1
251
+ while cursor is not None:
252
+ stands, cursor = section.stands.page(cursor, 50)
253
+ ```
254
+
255
+ `list(view)` gives a plain list. Deleting a parent is O(1): its index entries are purged in bounded steps by later writes, or by `Database.get_instance().purge_step()` from a timer.
233
256
 
234
257
  ## Namespaces
235
258
 
@@ -11,7 +11,7 @@ A lightweight key-value database with entity relationships and audit logging cap
11
11
  ## Features
12
12
 
13
13
  - **Persistent Storage**: Works with StableBTreeMap stable structure for persistent storage on your canister's stable memory so your data persists automatically across canister upgrades.
14
- - **Range-based paging**: `load_some`, `find`, `instances` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id.
14
+ - **Range-based paging**: `page`, `load_some` and `rebuild_field_index` read a page of entities with a single ordered `range` call on the stable map (Basilisk `StableBTreeMap.range`), so cost tracks the rows returned rather than the highest id ever issued, and deleted ids are free. Backends without `range` (older CDK builds, plain dict test doubles) transparently fall back to one read per id. `instances()` and `find()` load a whole type: they are deprecated and raise `TooLargeError` past 1,000 entities (use `count()`, `find_by()` or `page()`).
15
15
  - **Entity-Relational Database**: Create, read and write entities with OneToOne, OneToMany, ManyToOne, and ManyToMany relationships.
16
16
  - **Schema Versioning & Upgrade Safety**: Automatic schema introspection, compatibility checking, and auto-migration for safe changes. Breaking changes without a `migrate()` method are rejected, preventing data corruption on canister upgrades.
17
17
  - **Entity Hooks**: Intercept and control entity lifecycle events (create, modify, delete) with `on_event` hooks.
@@ -114,11 +114,12 @@ Then use the defined entities to store objects:
114
114
 
115
115
  assert Person.count() == 3
116
116
  assert Person.max_id() == 4
117
- assert Person.instances() == [john, alice, eva]
118
117
 
119
- # Cursor-based pagination
120
- assert Person.load_some(0, 2) == [john, alice]
121
- assert Person.load_some(2, 2) == [eva]
118
+ # Cursor-based pagination: (page, next_from_id), None on the last page
119
+ page, cursor = Person.page(1, 2)
120
+ assert page == [john, alice]
121
+ page, cursor = Person.page(cursor, 2)
122
+ assert page == [eva] and cursor is None
122
123
 
123
124
  # Retrieve database contents in JSON format
124
125
  print(Database.get_instance().dump_json(pretty=True))
@@ -180,7 +181,29 @@ while cursor is not None:
180
181
 
181
182
  The rebuild is idempotent. The `indexed` flag is not part of the schema descriptor, so toggling it never triggers upgrade-compatibility errors.
182
183
 
183
- Note: each distinct value's ID list is stored in a single storage entry, so extremely hot values are bounded by the backing store's `max_value_size` — the same constraint as relationship reverse indexes.
184
+ Each matching id is its own storage entry, so a value can match any number of entities without reaching the backing store's `max_value_size`.
185
+
186
+ ## Large Relations
187
+
188
+ `OneToMany` and `ManyToMany` getters return a `RelationView`, not a list. Questions about the relation and single pages never load every member, so they stay cheap however large it grows:
189
+
190
+ ```python
191
+ if section.stands: ... # one read
192
+ len(section.stands) # one read
193
+ stand in section.stands # two reads
194
+ stands, cursor = section.stands.page(1, 50) # one range read + 50 loads
195
+ group.members.add(user) # index writes only
196
+ ```
197
+
198
+ Iterating, indexing and comparing still work, but they load every member, so past `constants.MAX_UNPAGED` (1,000) they raise `TooLargeError` instead of running into the IC instruction limit. Walk a large relation with `page()`, across calls if needed:
199
+
200
+ ```python
201
+ cursor = 1
202
+ while cursor is not None:
203
+ stands, cursor = section.stands.page(cursor, 50)
204
+ ```
205
+
206
+ `list(view)` gives a plain list. Deleting a parent is O(1): its index entries are purged in bounded steps by later writes, or by `Database.get_instance().purge_step()` from a timer.
184
207
 
185
208
  ## Namespaces
186
209
 
@@ -5,6 +5,7 @@ IC Python DB - A lightweight key-value database with entity relationships and au
5
5
  from .constants import ACTION_CREATE, ACTION_DELETE, ACTION_MODIFY
6
6
  from .db_engine import Database
7
7
  from .entity import Entity
8
+ from .errors import TooLargeError
8
9
  from .mixins import TimestampedMixin
9
10
  from .properties import (
10
11
  Boolean,
@@ -14,13 +15,14 @@ from .properties import (
14
15
  ManyToOne,
15
16
  OneToMany,
16
17
  OneToOne,
18
+ RelationView,
17
19
  String,
18
20
  )
19
21
  from .schema import SchemaIncompatibleError, build_schema, diff_schemas, schema_hash
20
22
  from .storage import MemoryStorage, Storage
21
23
  from .system_time import SystemTime
22
24
 
23
- __version__ = "0.12.0"
25
+ __version__ = "0.13.0"
24
26
  __all__ = [
25
27
  "Database",
26
28
  "Entity",
@@ -40,6 +42,8 @@ __all__ = [
40
42
  "ACTION_MODIFY",
41
43
  "ACTION_DELETE",
42
44
  "SchemaIncompatibleError",
45
+ "TooLargeError",
46
+ "RelationView",
43
47
  "build_schema",
44
48
  "diff_schemas",
45
49
  "schema_hash",
@@ -0,0 +1,11 @@
1
+ LEVEL_MAX_DEFAULT = 3
2
+
3
+ # Hook action types
4
+ ACTION_CREATE = "create"
5
+ ACTION_MODIFY = "modify"
6
+ ACTION_DELETE = "delete"
7
+
8
+ # Largest collection an unpaged operation (iterating a relation, instances())
9
+ # loads in one call; past it they raise TooLargeError. At about 1M
10
+ # instructions per load this stays well inside a query's 5B budget.
11
+ MAX_UNPAGED = 1000