ic-python-db 0.13.0__tar.gz → 0.15.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.
- {ic_python_db-0.13.0/ic_python_db.egg-info → ic_python_db-0.15.0}/PKG-INFO +47 -1
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/README.md +46 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/__init__.py +1 -1
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/db_engine.py +524 -39
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/entity.py +255 -29
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/properties.py +20 -2
- {ic_python_db-0.13.0 → ic_python_db-0.15.0/ic_python_db.egg-info}/PKG-INFO +47 -1
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/pyproject.toml +1 -1
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/setup.py +1 -1
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/LICENSE +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/MANIFEST.in +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/_cdk.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/constants.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/context.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/errors.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/hooks.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/mixins.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/py.typed +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/schema.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/storage.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/system_time.py +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db.egg-info/SOURCES.txt +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db.egg-info/dependency_links.txt +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db.egg-info/top_level.txt +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/requirements-dev.txt +0 -0
- {ic_python_db-0.13.0 → ic_python_db-0.15.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ic-python-db
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.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
|
|
@@ -232,6 +232,52 @@ The rebuild is idempotent. The `indexed` flag is not part of the schema descript
|
|
|
232
232
|
|
|
233
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
234
|
|
|
235
|
+
### Values that hold `|`
|
|
236
|
+
|
|
237
|
+
An index entry is a key that starts with the value, then `|`, a letter and the id (`_fi:Ticket:status:open|n000000000042`). A value is text a caller chooses, and one that ended like such a key would put keys of its own among the entries of another value, where a page read could miss some of them. So a value with a `|` in it is escaped (`|` as `%7C`, and `%` as `%25`, so that two values never share an escaped form) and filed under `_fx` instead of `_fi`: no value can hold a `|`, so the keys of two values never meet. Every other value keeps the key it always had, one with only a `%` in it included. The escaped key is longer (two more characters for each `|` or `%`), which counts against the storage map's `max_key_size`.
|
|
238
|
+
|
|
239
|
+
**Upgrading from 0.14.** Entries of values with a `|`, filed before this under `_fi` with the value as it was, stay readable: `find_by`, `count_by` and `field_index_*` also read the old key of such a value, and adding or removing an id takes it off the old list. If 0.13 or 0.14 already ran on a store of 0.12 that holds the old lists of both a value `V` and `V|n` and 12 digits, or `V|h` and the digest of a custom id in `V`'s list, converting `V` wrote its entry over the other value's old list: that value's ids are lost. `Entity.rebuild_field_indexes` re-adds the sequential ones (run it, then the repair); a custom id comes back only when its entity's field is next changed, or when `Database.field_index_add` is called for it. To move them all, run once after upgrading, in bounded calls:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
db = Database.get_instance()
|
|
243
|
+
cursor = ""
|
|
244
|
+
while cursor is not None:
|
|
245
|
+
cursor = db.repair_field_indexes(cursor, limit=500) # keys read per call
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`repair_field_indexes` checks each entry of a value with a `|` against its entity: it moves an old entry to its present key and removes one whose entity is gone or has another value. It reaches custom ids and keys of any length, and it converts a list that a release before 0.13 stored as one array under the old key of such a value (a list counts its ids against `limit`; one longer than `limit` is converted whole, alone in its call). Nothing needs to run before it. `Entity.rebuild_field_indexes(from_id, batch)` does the move per type for sequential ids only, and adds entries that are missing; it never removes one.
|
|
249
|
+
|
|
250
|
+
When the repair has run to its end no old key holds an entry and counts are exact, except for what it cannot judge and leaves as it is: entries of a type or field the code does not index; entries whose row has no such field; an old-form list of more than 100 thousand ids (it is logged; a read of its value converts it where it fits the message, about 160 thousand ids in an update and 20 thousand in a query, and traps where it does not, as before); and an old-form list of a value made to end with `|h` and the digest of its own text, which reads as an entry of the value it extends (only such a value matches; `find_by` is not affected). A chain of old lists of `V`, `V|c`, `V|c|c` and so on deeper than about 500 raises `RecursionError` (also a value made for it). Only then may a release drop the old-key reads.
|
|
251
|
+
|
|
252
|
+
Cost, measured on the IC: a key read about 0.1 million instructions; an entry of a value with a `|` that is right about 0.73 million (its row is loaded); an old entry moved about 2 million, so a call of 500 moves is about 1 billion; and about 0.58 million for each distinct key length in the whole map that a call walks past. A call is O(`limit` + those lengths). An old-form list is converted whole at about 245 thousand instructions an id: a map with limited value size holds at most about 10 thousand ids in a list, but a realm's is not limited (a list of 127,000 ids is on record: about 31 billion instructions; it is over the cap of 100 thousand ids, so the repair leaves it, and a read of its value in an update converts it).
|
|
253
|
+
|
|
254
|
+
**Rolling back to a release before this one, and upgrading again.** While the older release runs, it files values with a `|` under `_fi` (this release reads them there), but it does not know `_fx`: entries filed under it are invisible to it, and it does not remove one when an entity's value changes or the entity is deleted. After the next upgrade a list can hold an entity that no longer has the value, or an id on both lists, so `count_by` and `find_by` can be wrong for those values. Run `repair_field_indexes` to its end again, **from `after=""`**: a cursor, or a note that it was done, saved before the rollback skips what the older release wrote. It puts it right.
|
|
255
|
+
|
|
256
|
+
## Sorted Fields
|
|
257
|
+
|
|
258
|
+
Declare `sorted=True` on an `Integer` or `Float` property to read entities in its order, highest or lowest first, without loading and sorting them all. `__sorted_indexes__` adds indexes over several fields, where each field breaks ties in the one before:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
class Listing(Entity):
|
|
262
|
+
name = String()
|
|
263
|
+
installs = Integer(sorted=True)
|
|
264
|
+
likes = Integer(sorted=True)
|
|
265
|
+
updated_at = Float(sorted=True)
|
|
266
|
+
__sorted_indexes__ = [("installs", "likes")] # installs, then likes
|
|
267
|
+
|
|
268
|
+
top, cursor = Listing.top_by("installs", count=20) # most installed
|
|
269
|
+
top, cursor = Listing.top_by(("installs", "likes"), count=20) # ties broken by likes
|
|
270
|
+
recent, cursor = Listing.top_by("updated_at", count=20)
|
|
271
|
+
oldest, cursor = Listing.top_by("updated_at", count=20, descending=False)
|
|
272
|
+
more, cursor = Listing.top_by("installs", count=20, cursor=cursor) # next page; None at the end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The storage map keeps keys in order, so each entity has one index entry per sorted index whose key starts with its values. Writing a sorted field moves that entry (a few storage operations), and a page is one range read plus `count` loads, however many entities exist. Ties go to the higher id; entities with None in a field are left out of indexes on it. Integers must fit in 64 bits.
|
|
276
|
+
|
|
277
|
+
`where=` drops entities that fail a test. At most `scan_limit` entries (default 200) are read per call, so with a selective filter a page can come back short with a cursor: call again.
|
|
278
|
+
|
|
279
|
+
Fields made sorted after rows exist, or whose type changed, need a resumable rebuild (`Listing.rebuild_sorted_index("installs", from_id=cursor)`, until it returns None). A `migrate()` that changes a sorted or indexed value moves the entity's entries when it is loaded.
|
|
280
|
+
|
|
235
281
|
## Large Relations
|
|
236
282
|
|
|
237
283
|
`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:
|
|
@@ -183,6 +183,52 @@ The rebuild is idempotent. The `indexed` flag is not part of the schema descript
|
|
|
183
183
|
|
|
184
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
185
|
|
|
186
|
+
### Values that hold `|`
|
|
187
|
+
|
|
188
|
+
An index entry is a key that starts with the value, then `|`, a letter and the id (`_fi:Ticket:status:open|n000000000042`). A value is text a caller chooses, and one that ended like such a key would put keys of its own among the entries of another value, where a page read could miss some of them. So a value with a `|` in it is escaped (`|` as `%7C`, and `%` as `%25`, so that two values never share an escaped form) and filed under `_fx` instead of `_fi`: no value can hold a `|`, so the keys of two values never meet. Every other value keeps the key it always had, one with only a `%` in it included. The escaped key is longer (two more characters for each `|` or `%`), which counts against the storage map's `max_key_size`.
|
|
189
|
+
|
|
190
|
+
**Upgrading from 0.14.** Entries of values with a `|`, filed before this under `_fi` with the value as it was, stay readable: `find_by`, `count_by` and `field_index_*` also read the old key of such a value, and adding or removing an id takes it off the old list. If 0.13 or 0.14 already ran on a store of 0.12 that holds the old lists of both a value `V` and `V|n` and 12 digits, or `V|h` and the digest of a custom id in `V`'s list, converting `V` wrote its entry over the other value's old list: that value's ids are lost. `Entity.rebuild_field_indexes` re-adds the sequential ones (run it, then the repair); a custom id comes back only when its entity's field is next changed, or when `Database.field_index_add` is called for it. To move them all, run once after upgrading, in bounded calls:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
db = Database.get_instance()
|
|
194
|
+
cursor = ""
|
|
195
|
+
while cursor is not None:
|
|
196
|
+
cursor = db.repair_field_indexes(cursor, limit=500) # keys read per call
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`repair_field_indexes` checks each entry of a value with a `|` against its entity: it moves an old entry to its present key and removes one whose entity is gone or has another value. It reaches custom ids and keys of any length, and it converts a list that a release before 0.13 stored as one array under the old key of such a value (a list counts its ids against `limit`; one longer than `limit` is converted whole, alone in its call). Nothing needs to run before it. `Entity.rebuild_field_indexes(from_id, batch)` does the move per type for sequential ids only, and adds entries that are missing; it never removes one.
|
|
200
|
+
|
|
201
|
+
When the repair has run to its end no old key holds an entry and counts are exact, except for what it cannot judge and leaves as it is: entries of a type or field the code does not index; entries whose row has no such field; an old-form list of more than 100 thousand ids (it is logged; a read of its value converts it where it fits the message, about 160 thousand ids in an update and 20 thousand in a query, and traps where it does not, as before); and an old-form list of a value made to end with `|h` and the digest of its own text, which reads as an entry of the value it extends (only such a value matches; `find_by` is not affected). A chain of old lists of `V`, `V|c`, `V|c|c` and so on deeper than about 500 raises `RecursionError` (also a value made for it). Only then may a release drop the old-key reads.
|
|
202
|
+
|
|
203
|
+
Cost, measured on the IC: a key read about 0.1 million instructions; an entry of a value with a `|` that is right about 0.73 million (its row is loaded); an old entry moved about 2 million, so a call of 500 moves is about 1 billion; and about 0.58 million for each distinct key length in the whole map that a call walks past. A call is O(`limit` + those lengths). An old-form list is converted whole at about 245 thousand instructions an id: a map with limited value size holds at most about 10 thousand ids in a list, but a realm's is not limited (a list of 127,000 ids is on record: about 31 billion instructions; it is over the cap of 100 thousand ids, so the repair leaves it, and a read of its value in an update converts it).
|
|
204
|
+
|
|
205
|
+
**Rolling back to a release before this one, and upgrading again.** While the older release runs, it files values with a `|` under `_fi` (this release reads them there), but it does not know `_fx`: entries filed under it are invisible to it, and it does not remove one when an entity's value changes or the entity is deleted. After the next upgrade a list can hold an entity that no longer has the value, or an id on both lists, so `count_by` and `find_by` can be wrong for those values. Run `repair_field_indexes` to its end again, **from `after=""`**: a cursor, or a note that it was done, saved before the rollback skips what the older release wrote. It puts it right.
|
|
206
|
+
|
|
207
|
+
## Sorted Fields
|
|
208
|
+
|
|
209
|
+
Declare `sorted=True` on an `Integer` or `Float` property to read entities in its order, highest or lowest first, without loading and sorting them all. `__sorted_indexes__` adds indexes over several fields, where each field breaks ties in the one before:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
class Listing(Entity):
|
|
213
|
+
name = String()
|
|
214
|
+
installs = Integer(sorted=True)
|
|
215
|
+
likes = Integer(sorted=True)
|
|
216
|
+
updated_at = Float(sorted=True)
|
|
217
|
+
__sorted_indexes__ = [("installs", "likes")] # installs, then likes
|
|
218
|
+
|
|
219
|
+
top, cursor = Listing.top_by("installs", count=20) # most installed
|
|
220
|
+
top, cursor = Listing.top_by(("installs", "likes"), count=20) # ties broken by likes
|
|
221
|
+
recent, cursor = Listing.top_by("updated_at", count=20)
|
|
222
|
+
oldest, cursor = Listing.top_by("updated_at", count=20, descending=False)
|
|
223
|
+
more, cursor = Listing.top_by("installs", count=20, cursor=cursor) # next page; None at the end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The storage map keeps keys in order, so each entity has one index entry per sorted index whose key starts with its values. Writing a sorted field moves that entry (a few storage operations), and a page is one range read plus `count` loads, however many entities exist. Ties go to the higher id; entities with None in a field are left out of indexes on it. Integers must fit in 64 bits.
|
|
227
|
+
|
|
228
|
+
`where=` drops entities that fail a test. At most `scan_limit` entries (default 200) are read per call, so with a selective filter a page can come back short with a cursor: call again.
|
|
229
|
+
|
|
230
|
+
Fields made sorted after rows exist, or whose type changed, need a resumable rebuild (`Listing.rebuild_sorted_index("installs", from_id=cursor)`, until it returns None). A `migrate()` that changes a sorted or indexed value moves the entity's entries when it is loaded.
|
|
231
|
+
|
|
186
232
|
## Large Relations
|
|
187
233
|
|
|
188
234
|
`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:
|
|
@@ -22,7 +22,7 @@ from .schema import SchemaIncompatibleError, build_schema, diff_schemas, schema_
|
|
|
22
22
|
from .storage import MemoryStorage, Storage
|
|
23
23
|
from .system_time import SystemTime
|
|
24
24
|
|
|
25
|
-
__version__ = "0.
|
|
25
|
+
__version__ = "0.15.0"
|
|
26
26
|
__all__ = [
|
|
27
27
|
"Database",
|
|
28
28
|
"Entity",
|