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.
Files changed (26) hide show
  1. {ic_python_db-0.13.0/ic_python_db.egg-info → ic_python_db-0.15.0}/PKG-INFO +47 -1
  2. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/README.md +46 -0
  3. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/__init__.py +1 -1
  4. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/db_engine.py +524 -39
  5. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/entity.py +255 -29
  6. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/properties.py +20 -2
  7. {ic_python_db-0.13.0 → ic_python_db-0.15.0/ic_python_db.egg-info}/PKG-INFO +47 -1
  8. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/pyproject.toml +1 -1
  9. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/setup.py +1 -1
  10. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/LICENSE +0 -0
  11. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/MANIFEST.in +0 -0
  12. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/_cdk.py +0 -0
  13. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/constants.py +0 -0
  14. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/context.py +0 -0
  15. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/errors.py +0 -0
  16. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/hooks.py +0 -0
  17. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/mixins.py +0 -0
  18. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/py.typed +0 -0
  19. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/schema.py +0 -0
  20. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/storage.py +0 -0
  21. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db/system_time.py +0 -0
  22. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db.egg-info/SOURCES.txt +0 -0
  23. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db.egg-info/dependency_links.txt +0 -0
  24. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/ic_python_db.egg-info/top_level.txt +0 -0
  25. {ic_python_db-0.13.0 → ic_python_db-0.15.0}/requirements-dev.txt +0 -0
  26. {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.13.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.13.0"
25
+ __version__ = "0.15.0"
26
26
  __all__ = [
27
27
  "Database",
28
28
  "Entity",