event-reduce 7.4.2__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.
- event_reduce-7.4.2/PKG-INFO +135 -0
- event_reduce-7.4.2/README.md +124 -0
- event_reduce-7.4.2/pyproject.toml +38 -0
- event_reduce-7.4.2/setup.cfg +4 -0
- event_reduce-7.4.2/src/event_reduce/__init__.py +89 -0
- event_reduce-7.4.2/src/event_reduce/actions.py +183 -0
- event_reduce-7.4.2/src/event_reduce/bdd.py +38 -0
- event_reduce-7.4.2/src/event_reduce/bdd_generated.py +4 -0
- event_reduce-7.4.2/src/event_reduce/py.typed +0 -0
- event_reduce-7.4.2/src/event_reduce/result_window_buffer.py +356 -0
- event_reduce-7.4.2/src/event_reduce/states.py +184 -0
- event_reduce-7.4.2/src/event_reduce/types.py +140 -0
- event_reduce-7.4.2/src/event_reduce/util.py +110 -0
- event_reduce-7.4.2/src/event_reduce.egg-info/PKG-INFO +135 -0
- event_reduce-7.4.2/src/event_reduce.egg-info/SOURCES.txt +16 -0
- event_reduce-7.4.2/src/event_reduce.egg-info/dependency_links.txt +1 -0
- event_reduce-7.4.2/src/event_reduce.egg-info/top_level.txt +1 -0
- event_reduce-7.4.2/tests/test_suite.py +147 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: event-reduce
|
|
3
|
+
Version: 7.4.2
|
|
4
|
+
Summary: Update live query results from database change events without re-running the query.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/pubkey/event-reduce
|
|
7
|
+
Project-URL: Repository, https://github.com/pubkey/event-reduce
|
|
8
|
+
Keywords: live-query,reactive,realtime,database,bdd
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# event-reduce (Python)
|
|
13
|
+
|
|
14
|
+
Python implementation of the [EventReduce algorithm](https://github.com/pubkey/event-reduce).
|
|
15
|
+
It takes the previous results of a query and a change event of the database
|
|
16
|
+
and calculates the new results **in memory, without running the query over the database again**.
|
|
17
|
+
|
|
18
|
+
It uses the BDD that the [JavaScript implementation](../javascript/) created,
|
|
19
|
+
so both always decide on the same action. It needs Python 3.10 or newer and has no dependencies.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pip install event-reduce
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from event_reduce import (
|
|
31
|
+
ActionName,
|
|
32
|
+
ChangeEvent,
|
|
33
|
+
StateResolveFunctionInput,
|
|
34
|
+
calculate_action_name,
|
|
35
|
+
dict_query_params,
|
|
36
|
+
run_action,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def compare(a, b):
|
|
41
|
+
# must never return 0 for two different documents, so sort by the primary key last
|
|
42
|
+
if a["age"] != b["age"]:
|
|
43
|
+
return -1 if a["age"] < b["age"] else 1
|
|
44
|
+
return -1 if a["id"] < b["id"] else 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# SELECT * FROM users WHERE age > 18 ORDER BY age, id LIMIT 10
|
|
48
|
+
query_params = dict_query_params(
|
|
49
|
+
primary_key="id",
|
|
50
|
+
sort_fields=["age", "id"],
|
|
51
|
+
query_matcher=lambda doc: doc["age"] > 18,
|
|
52
|
+
sort_comparator=compare,
|
|
53
|
+
limit=10,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
results = [{"id": "a", "age": 20}, {"id": "b", "age": 30}]
|
|
57
|
+
|
|
58
|
+
# a change event from the change stream of your database
|
|
59
|
+
change_event = ChangeEvent.insert("c", {"id": "c", "age": 25})
|
|
60
|
+
|
|
61
|
+
action = calculate_action_name(StateResolveFunctionInput(query_params, change_event, results))
|
|
62
|
+
if action == ActionName.RUN_FULL_QUERY_AGAIN:
|
|
63
|
+
results = run_query_on_database() # EventReduce could not optimize this event
|
|
64
|
+
else:
|
|
65
|
+
run_action(action, query_params, change_event, results)
|
|
66
|
+
# -> [a, c, b]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`dict_query_params()` is for dict documents, like the `primaryKey` and `sortFields` of the JavaScript implementation.
|
|
70
|
+
The sort fields can be dot-separated paths like `"address.city"`.
|
|
71
|
+
For other documents, create the `QueryParams` with a `get_document_id` function
|
|
72
|
+
and a `sort_fields_changed(previous, doc)` function that returns true if a sort field changed.
|
|
73
|
+
|
|
74
|
+
### Key document map
|
|
75
|
+
|
|
76
|
+
For big result sets, pass a dict from the id to the document as `key_document_map`
|
|
77
|
+
to both `StateResolveFunctionInput` and `run_action()`.
|
|
78
|
+
It is used to check whether a document is in the results without scanning them,
|
|
79
|
+
and `run_action()` keeps it in sync with the results.
|
|
80
|
+
|
|
81
|
+
### Differences to the JavaScript implementation
|
|
82
|
+
|
|
83
|
+
- Change events are created with `ChangeEvent.insert()`, `ChangeEvent.update()` and `ChangeEvent.delete()`.
|
|
84
|
+
- The names are snake_case, `ActionName` and `StateName` are enums whose values are the JavaScript names.
|
|
85
|
+
- `run_action()` raises an `EventReduceError` for `RUN_FULL_QUERY_AGAIN` and `UNKNOWN_ACTION`.
|
|
86
|
+
`ALWAYS_WRONG` is only used by the truth-table generator and raises like `UNKNOWN_ACTION`.
|
|
87
|
+
|
|
88
|
+
## Result window buffer
|
|
89
|
+
|
|
90
|
+
For a query with a limit or a skip, EventReduce returns `RUN_FULL_QUERY_AGAIN` when a row has to move into the result window that is not in the previous results:
|
|
91
|
+
the row after the window (when a row leaves a full window) or the last skipped row (when a row is added in front of the window).
|
|
92
|
+
The result window buffer avoids most of these full queries: it keeps `limit_buffer_size` rows after the window and `skip_buffer_size` skipped rows before it in memory.
|
|
93
|
+
It works like the [result window buffer of the JavaScript implementation](../docs/result-window-buffer.md),
|
|
94
|
+
which explains how it works, how to choose the buffer sizes and has measurements.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from event_reduce import ResultWindowBuffer, get_recommended_result_window_buffer_size
|
|
98
|
+
|
|
99
|
+
# recommended size: about log2(limit), at most 30
|
|
100
|
+
size = get_recommended_result_window_buffer_size(query_params.limit)
|
|
101
|
+
buffer = ResultWindowBuffer(query_params, limit_buffer_size=size, skip_buffer_size=size)
|
|
102
|
+
# run the query with the skip and limit of the buffer
|
|
103
|
+
buffer.reset(run_query(buffer.query()))
|
|
104
|
+
# the rows of the original skip and limit
|
|
105
|
+
results = buffer.results()
|
|
106
|
+
|
|
107
|
+
# on new change events
|
|
108
|
+
result = buffer.apply_change_events(change_events)
|
|
109
|
+
if result.run_full_query_again:
|
|
110
|
+
buffer.reset(run_query(buffer.query()))
|
|
111
|
+
results = buffer.results()
|
|
112
|
+
elif result.changed:
|
|
113
|
+
results = result.results
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`query()` returns the `skip` and `limit` to run the query with, `limit` is None if all rows after the skip are needed.
|
|
117
|
+
`math.inf` as a buffer size keeps all rows on that side, like `Infinity` in JavaScript.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
pip install pytest ruff mypy
|
|
123
|
+
pytest
|
|
124
|
+
ruff check . && ruff format --check . && mypy src
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The BDD is generated by the truth-table generator of the JavaScript implementation,
|
|
128
|
+
which also writes `src/event_reduce/bdd_generated.py`.
|
|
129
|
+
The tests run the [shared test suite](../test-suite/), the same tests as all other implementations.
|
|
130
|
+
|
|
131
|
+
## Who uses EventReduce
|
|
132
|
+
|
|
133
|
+
[RxDB](https://rxdb.info/), the local-first database, uses EventReduce for its live queries since [version 9.0.0](https://rxdb.info/releases/9.0.0.html), where it is enabled by default.
|
|
134
|
+
When an app subscribes to a query, RxDB keeps the results up to date by running each write event through EventReduce,
|
|
135
|
+
and only queries the storage again when EventReduce returns `RUN_FULL_QUERY_AGAIN`.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# event-reduce (Python)
|
|
2
|
+
|
|
3
|
+
Python implementation of the [EventReduce algorithm](https://github.com/pubkey/event-reduce).
|
|
4
|
+
It takes the previous results of a query and a change event of the database
|
|
5
|
+
and calculates the new results **in memory, without running the query over the database again**.
|
|
6
|
+
|
|
7
|
+
It uses the BDD that the [JavaScript implementation](../javascript/) created,
|
|
8
|
+
so both always decide on the same action. It needs Python 3.10 or newer and has no dependencies.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pip install event-reduce
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from event_reduce import (
|
|
20
|
+
ActionName,
|
|
21
|
+
ChangeEvent,
|
|
22
|
+
StateResolveFunctionInput,
|
|
23
|
+
calculate_action_name,
|
|
24
|
+
dict_query_params,
|
|
25
|
+
run_action,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compare(a, b):
|
|
30
|
+
# must never return 0 for two different documents, so sort by the primary key last
|
|
31
|
+
if a["age"] != b["age"]:
|
|
32
|
+
return -1 if a["age"] < b["age"] else 1
|
|
33
|
+
return -1 if a["id"] < b["id"] else 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# SELECT * FROM users WHERE age > 18 ORDER BY age, id LIMIT 10
|
|
37
|
+
query_params = dict_query_params(
|
|
38
|
+
primary_key="id",
|
|
39
|
+
sort_fields=["age", "id"],
|
|
40
|
+
query_matcher=lambda doc: doc["age"] > 18,
|
|
41
|
+
sort_comparator=compare,
|
|
42
|
+
limit=10,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
results = [{"id": "a", "age": 20}, {"id": "b", "age": 30}]
|
|
46
|
+
|
|
47
|
+
# a change event from the change stream of your database
|
|
48
|
+
change_event = ChangeEvent.insert("c", {"id": "c", "age": 25})
|
|
49
|
+
|
|
50
|
+
action = calculate_action_name(StateResolveFunctionInput(query_params, change_event, results))
|
|
51
|
+
if action == ActionName.RUN_FULL_QUERY_AGAIN:
|
|
52
|
+
results = run_query_on_database() # EventReduce could not optimize this event
|
|
53
|
+
else:
|
|
54
|
+
run_action(action, query_params, change_event, results)
|
|
55
|
+
# -> [a, c, b]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`dict_query_params()` is for dict documents, like the `primaryKey` and `sortFields` of the JavaScript implementation.
|
|
59
|
+
The sort fields can be dot-separated paths like `"address.city"`.
|
|
60
|
+
For other documents, create the `QueryParams` with a `get_document_id` function
|
|
61
|
+
and a `sort_fields_changed(previous, doc)` function that returns true if a sort field changed.
|
|
62
|
+
|
|
63
|
+
### Key document map
|
|
64
|
+
|
|
65
|
+
For big result sets, pass a dict from the id to the document as `key_document_map`
|
|
66
|
+
to both `StateResolveFunctionInput` and `run_action()`.
|
|
67
|
+
It is used to check whether a document is in the results without scanning them,
|
|
68
|
+
and `run_action()` keeps it in sync with the results.
|
|
69
|
+
|
|
70
|
+
### Differences to the JavaScript implementation
|
|
71
|
+
|
|
72
|
+
- Change events are created with `ChangeEvent.insert()`, `ChangeEvent.update()` and `ChangeEvent.delete()`.
|
|
73
|
+
- The names are snake_case, `ActionName` and `StateName` are enums whose values are the JavaScript names.
|
|
74
|
+
- `run_action()` raises an `EventReduceError` for `RUN_FULL_QUERY_AGAIN` and `UNKNOWN_ACTION`.
|
|
75
|
+
`ALWAYS_WRONG` is only used by the truth-table generator and raises like `UNKNOWN_ACTION`.
|
|
76
|
+
|
|
77
|
+
## Result window buffer
|
|
78
|
+
|
|
79
|
+
For a query with a limit or a skip, EventReduce returns `RUN_FULL_QUERY_AGAIN` when a row has to move into the result window that is not in the previous results:
|
|
80
|
+
the row after the window (when a row leaves a full window) or the last skipped row (when a row is added in front of the window).
|
|
81
|
+
The result window buffer avoids most of these full queries: it keeps `limit_buffer_size` rows after the window and `skip_buffer_size` skipped rows before it in memory.
|
|
82
|
+
It works like the [result window buffer of the JavaScript implementation](../docs/result-window-buffer.md),
|
|
83
|
+
which explains how it works, how to choose the buffer sizes and has measurements.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from event_reduce import ResultWindowBuffer, get_recommended_result_window_buffer_size
|
|
87
|
+
|
|
88
|
+
# recommended size: about log2(limit), at most 30
|
|
89
|
+
size = get_recommended_result_window_buffer_size(query_params.limit)
|
|
90
|
+
buffer = ResultWindowBuffer(query_params, limit_buffer_size=size, skip_buffer_size=size)
|
|
91
|
+
# run the query with the skip and limit of the buffer
|
|
92
|
+
buffer.reset(run_query(buffer.query()))
|
|
93
|
+
# the rows of the original skip and limit
|
|
94
|
+
results = buffer.results()
|
|
95
|
+
|
|
96
|
+
# on new change events
|
|
97
|
+
result = buffer.apply_change_events(change_events)
|
|
98
|
+
if result.run_full_query_again:
|
|
99
|
+
buffer.reset(run_query(buffer.query()))
|
|
100
|
+
results = buffer.results()
|
|
101
|
+
elif result.changed:
|
|
102
|
+
results = result.results
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`query()` returns the `skip` and `limit` to run the query with, `limit` is None if all rows after the skip are needed.
|
|
106
|
+
`math.inf` as a buffer size keeps all rows on that side, like `Infinity` in JavaScript.
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
pip install pytest ruff mypy
|
|
112
|
+
pytest
|
|
113
|
+
ruff check . && ruff format --check . && mypy src
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The BDD is generated by the truth-table generator of the JavaScript implementation,
|
|
117
|
+
which also writes `src/event_reduce/bdd_generated.py`.
|
|
118
|
+
The tests run the [shared test suite](../test-suite/), the same tests as all other implementations.
|
|
119
|
+
|
|
120
|
+
## Who uses EventReduce
|
|
121
|
+
|
|
122
|
+
[RxDB](https://rxdb.info/), the local-first database, uses EventReduce for its live queries since [version 9.0.0](https://rxdb.info/releases/9.0.0.html), where it is enabled by default.
|
|
123
|
+
When an app subscribes to a query, RxDB keeps the results up to date by running each write event through EventReduce,
|
|
124
|
+
and only queries the storage again when EventReduce returns `RUN_FULL_QUERY_AGAIN`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "event-reduce"
|
|
7
|
+
version = "7.4.2"
|
|
8
|
+
description = "Update live query results from database change events without re-running the query."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["live-query", "reactive", "realtime", "database", "bdd"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Homepage = "https://github.com/pubkey/event-reduce"
|
|
16
|
+
Repository = "https://github.com/pubkey/event-reduce"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.package-data]
|
|
19
|
+
event_reduce = ["py.typed"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
23
|
+
pythonpath = ["src"]
|
|
24
|
+
|
|
25
|
+
[tool.ruff]
|
|
26
|
+
line-length = 120
|
|
27
|
+
target-version = "py310"
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint]
|
|
30
|
+
select = ["E", "F", "W", "I", "UP", "B"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff.lint.per-file-ignores]
|
|
33
|
+
# generated, the packed BDD string is one long line
|
|
34
|
+
"src/event_reduce/bdd_generated.py" = ["E501"]
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
strict = true
|
|
38
|
+
python_version = "3.10"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EventReduce: update live query results from change events without re-running the query.
|
|
3
|
+
|
|
4
|
+
Only the BDD that the JavaScript implementation generated is used, it is not created or optimized here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
from .actions import ACTION_FUNCTION_LIST, ORDERED_ACTION_LIST, ActionFunction
|
|
12
|
+
from .bdd import resolve_input
|
|
13
|
+
from .bdd_generated import PACKED_BDD
|
|
14
|
+
from .result_window_buffer import (
|
|
15
|
+
ResultWindowBuffer,
|
|
16
|
+
ResultWindowBufferChangeResult,
|
|
17
|
+
ResultWindowBufferQuery,
|
|
18
|
+
ResultWindowBufferResult,
|
|
19
|
+
get_recommended_result_window_buffer_size,
|
|
20
|
+
)
|
|
21
|
+
from .states import ORDERED_STATE_LIST, get_state_set, resolve_state
|
|
22
|
+
from .types import (
|
|
23
|
+
ActionName,
|
|
24
|
+
ChangeEvent,
|
|
25
|
+
EventReduceError,
|
|
26
|
+
QueryParams,
|
|
27
|
+
ResultKeyDocumentMap,
|
|
28
|
+
StateName,
|
|
29
|
+
StateResolveFunctionInput,
|
|
30
|
+
WriteOperation,
|
|
31
|
+
)
|
|
32
|
+
from .util import dict_query_params, get_property
|
|
33
|
+
|
|
34
|
+
D = TypeVar("D")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def calculate_action_name(input: StateResolveFunctionInput[Any]) -> ActionName:
|
|
38
|
+
"""
|
|
39
|
+
Returns the action that transforms the previous results into the new results.
|
|
40
|
+
If it is RUN_FULL_QUERY_AGAIN, the query must be run over the database again.
|
|
41
|
+
"""
|
|
42
|
+
return ORDERED_ACTION_LIST[resolve_input(input)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_action(
|
|
46
|
+
action: ActionName,
|
|
47
|
+
query_params: QueryParams[D],
|
|
48
|
+
change_event: ChangeEvent[D],
|
|
49
|
+
previous_results: list[D],
|
|
50
|
+
key_document_map: dict[str, D] | None = None,
|
|
51
|
+
) -> list[D]:
|
|
52
|
+
"""
|
|
53
|
+
Applies the action to the results.
|
|
54
|
+
For performance reasons, it mutates previous_results and the key_document_map
|
|
55
|
+
and returns the mutated results.
|
|
56
|
+
Raises an EventReduceError for RUN_FULL_QUERY_AGAIN and UNKNOWN_ACTION.
|
|
57
|
+
"""
|
|
58
|
+
ACTION_FUNCTION_LIST[ORDERED_ACTION_LIST.index(action)](
|
|
59
|
+
StateResolveFunctionInput(query_params, change_event, previous_results, key_document_map)
|
|
60
|
+
)
|
|
61
|
+
return previous_results
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"ORDERED_ACTION_LIST",
|
|
66
|
+
"ORDERED_STATE_LIST",
|
|
67
|
+
"PACKED_BDD",
|
|
68
|
+
"ActionFunction",
|
|
69
|
+
"ActionName",
|
|
70
|
+
"ChangeEvent",
|
|
71
|
+
"EventReduceError",
|
|
72
|
+
"QueryParams",
|
|
73
|
+
"ResultKeyDocumentMap",
|
|
74
|
+
"ResultWindowBuffer",
|
|
75
|
+
"ResultWindowBufferChangeResult",
|
|
76
|
+
"ResultWindowBufferQuery",
|
|
77
|
+
"ResultWindowBufferResult",
|
|
78
|
+
"StateName",
|
|
79
|
+
"StateResolveFunctionInput",
|
|
80
|
+
"WriteOperation",
|
|
81
|
+
"calculate_action_name",
|
|
82
|
+
"dict_query_params",
|
|
83
|
+
"get_property",
|
|
84
|
+
"get_recommended_result_window_buffer_size",
|
|
85
|
+
"get_state_set",
|
|
86
|
+
"resolve_input",
|
|
87
|
+
"resolve_state",
|
|
88
|
+
"run_action",
|
|
89
|
+
]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .types import ActionName, EventReduceError, StateResolveFunctionInput
|
|
7
|
+
from .util import index_of_document_id, push_at_sort_position
|
|
8
|
+
|
|
9
|
+
ActionFunction = Callable[[StateResolveFunctionInput[Any]], None]
|
|
10
|
+
"""For performance reasons, action functions mutate the results and the key document map of the input."""
|
|
11
|
+
|
|
12
|
+
ORDERED_ACTION_LIST: list[ActionName] = list(ActionName)
|
|
13
|
+
"""All actions, in the same order as the JavaScript implementation. The BDD references the actions by their index."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_index_of_changed_doc(input: StateResolveFunctionInput[Any]) -> int:
|
|
17
|
+
"""Returns the index of the changed document inside of the previous results or -1."""
|
|
18
|
+
id = input.change_event.id
|
|
19
|
+
if input.key_document_map is not None and id not in input.key_document_map:
|
|
20
|
+
return -1
|
|
21
|
+
return index_of_document_id(input.query_params, input.previous_results, id)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _changed_doc(input: StateResolveFunctionInput[Any]) -> Any:
|
|
25
|
+
"""The new document of the change event. Only the BDD knows that it exists for the given action."""
|
|
26
|
+
doc = input.change_event.doc
|
|
27
|
+
if doc is None:
|
|
28
|
+
raise EventReduceError("UNK_ACT")
|
|
29
|
+
return doc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _add_to_map(input: StateResolveFunctionInput[Any], doc: Any) -> None:
|
|
33
|
+
if input.key_document_map is not None:
|
|
34
|
+
input.key_document_map[input.change_event.id] = doc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _remove_from_map(input: StateResolveFunctionInput[Any], id: str) -> None:
|
|
38
|
+
if input.key_document_map is not None:
|
|
39
|
+
input.key_document_map.pop(id, None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def do_nothing(input: StateResolveFunctionInput[Any]) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def insert_first(input: StateResolveFunctionInput[Any]) -> None:
|
|
47
|
+
doc = _changed_doc(input)
|
|
48
|
+
input.previous_results.insert(0, doc)
|
|
49
|
+
_add_to_map(input, doc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def insert_last(input: StateResolveFunctionInput[Any]) -> None:
|
|
53
|
+
doc = _changed_doc(input)
|
|
54
|
+
input.previous_results.append(doc)
|
|
55
|
+
_add_to_map(input, doc)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def remove_first_item(input: StateResolveFunctionInput[Any]) -> None:
|
|
59
|
+
if not input.previous_results:
|
|
60
|
+
return
|
|
61
|
+
first = input.previous_results.pop(0)
|
|
62
|
+
_remove_from_map(input, input.query_params.get_document_id(first))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def remove_last_item(input: StateResolveFunctionInput[Any]) -> None:
|
|
66
|
+
if not input.previous_results:
|
|
67
|
+
return
|
|
68
|
+
last = input.previous_results.pop()
|
|
69
|
+
_remove_from_map(input, input.query_params.get_document_id(last))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def remove_first_insert_last(input: StateResolveFunctionInput[Any]) -> None:
|
|
73
|
+
remove_first_item(input)
|
|
74
|
+
insert_last(input)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def remove_last_insert_first(input: StateResolveFunctionInput[Any]) -> None:
|
|
78
|
+
remove_last_item(input)
|
|
79
|
+
insert_first(input)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def remove_first_insert_first(input: StateResolveFunctionInput[Any]) -> None:
|
|
83
|
+
remove_first_item(input)
|
|
84
|
+
insert_first(input)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def remove_last_insert_last(input: StateResolveFunctionInput[Any]) -> None:
|
|
88
|
+
remove_last_item(input)
|
|
89
|
+
insert_last(input)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def remove_existing(input: StateResolveFunctionInput[Any]) -> None:
|
|
93
|
+
index = _find_index_of_changed_doc(input)
|
|
94
|
+
_remove_from_map(input, input.change_event.id)
|
|
95
|
+
if index >= 0:
|
|
96
|
+
del input.previous_results[index]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def replace_existing(input: StateResolveFunctionInput[Any]) -> None:
|
|
100
|
+
index = _find_index_of_changed_doc(input)
|
|
101
|
+
if index >= 0:
|
|
102
|
+
doc = _changed_doc(input)
|
|
103
|
+
input.previous_results[index] = doc
|
|
104
|
+
_add_to_map(input, doc)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def insert_at_sort_position(input: StateResolveFunctionInput[Any]) -> None:
|
|
108
|
+
doc = _changed_doc(input)
|
|
109
|
+
id = input.change_event.id
|
|
110
|
+
key_document_map = input.key_document_map
|
|
111
|
+
# If document is already in results,
|
|
112
|
+
# we cannot add it again because it would throw on non-deterministic ordering.
|
|
113
|
+
if key_document_map is not None:
|
|
114
|
+
if id in key_document_map:
|
|
115
|
+
return
|
|
116
|
+
key_document_map[id] = doc
|
|
117
|
+
elif index_of_document_id(input.query_params, input.previous_results, id) >= 0:
|
|
118
|
+
return
|
|
119
|
+
push_at_sort_position(input.previous_results, doc, input.query_params.sort_comparator)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def remove_existing_and_insert_at_sort_position(input: StateResolveFunctionInput[Any]) -> None:
|
|
123
|
+
remove_existing(input)
|
|
124
|
+
insert_at_sort_position(input)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _insert_at_sort_position_and_remove(input: StateResolveFunctionInput[Any], remove: ActionFunction) -> None:
|
|
128
|
+
length_before = len(input.previous_results)
|
|
129
|
+
insert_at_sort_position(input)
|
|
130
|
+
# insert_at_sort_position() does nothing if the document is already in the results
|
|
131
|
+
if len(input.previous_results) > length_before:
|
|
132
|
+
remove(input)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def insert_at_sort_position_remove_last(input: StateResolveFunctionInput[Any]) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Inserts the document at its sort position and then removes the last document.
|
|
138
|
+
Used when the limit was reached and the new document
|
|
139
|
+
sorts in between the first and the last document,
|
|
140
|
+
so the previous last document is pushed out of the results.
|
|
141
|
+
"""
|
|
142
|
+
_insert_at_sort_position_and_remove(input, remove_last_item)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def insert_at_sort_position_remove_first(input: StateResolveFunctionInput[Any]) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Inserts the document at its sort position and then removes the first document.
|
|
148
|
+
Used when a document moves out of the skipped documents into the results,
|
|
149
|
+
so the previous first document is pushed into the skipped documents.
|
|
150
|
+
"""
|
|
151
|
+
_insert_at_sort_position_and_remove(input, remove_first_item)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def run_full_query_again(input: StateResolveFunctionInput[Any]) -> None:
|
|
155
|
+
raise EventReduceError("RUN_FULL")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def unknown_action(input: StateResolveFunctionInput[Any]) -> None:
|
|
159
|
+
raise EventReduceError("UNK_ACT")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
ACTION_FUNCTION_LIST: list[ActionFunction] = [
|
|
163
|
+
do_nothing,
|
|
164
|
+
insert_first,
|
|
165
|
+
insert_last,
|
|
166
|
+
remove_first_item,
|
|
167
|
+
remove_last_item,
|
|
168
|
+
remove_first_insert_last,
|
|
169
|
+
remove_last_insert_first,
|
|
170
|
+
remove_first_insert_first,
|
|
171
|
+
remove_last_insert_last,
|
|
172
|
+
remove_existing,
|
|
173
|
+
replace_existing,
|
|
174
|
+
# alwaysWrong is only used by the truth-table generator and never returned by the BDD
|
|
175
|
+
unknown_action,
|
|
176
|
+
insert_at_sort_position,
|
|
177
|
+
remove_existing_and_insert_at_sort_position,
|
|
178
|
+
insert_at_sort_position_remove_last,
|
|
179
|
+
insert_at_sort_position_remove_first,
|
|
180
|
+
run_full_query_again,
|
|
181
|
+
unknown_action,
|
|
182
|
+
]
|
|
183
|
+
"""The action functions in the same order as ORDERED_ACTION_LIST."""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resolves the action with the BDD of the JavaScript implementation.
|
|
3
|
+
Only the already generated BDD is used, it is not created or optimized here.
|
|
4
|
+
|
|
5
|
+
The BDD is a PackedBdd of the binary-decision-diagram npm package,
|
|
6
|
+
bdd_generated.PACKED_BDD holds its decoded entries.
|
|
7
|
+
|
|
8
|
+
Layout of the entries:
|
|
9
|
+
- Every non-leaf node takes three consecutive entries: [level, ref0, ref1]
|
|
10
|
+
- level is the index of the state function in ORDERED_STATE_LIST
|
|
11
|
+
- ref0 is followed when the state is false, ref1 when it is true
|
|
12
|
+
- The node with the id x starts at index 3 * x, the root node has the id 0.
|
|
13
|
+
- With n = len(entries) / 3, a reference r < n points to the node with the id r,
|
|
14
|
+
r >= n is a leaf with the action index r - n in ORDERED_ACTION_LIST.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .bdd_generated import PACKED_BDD
|
|
22
|
+
from .states import STATE_RESOLVE_FUNCTION_BY_INDEX
|
|
23
|
+
from .types import StateResolveFunctionInput
|
|
24
|
+
|
|
25
|
+
_NODE_COUNT = len(PACKED_BDD) // 3
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_input(input: StateResolveFunctionInput[Any]) -> int:
|
|
29
|
+
"""Walks the BDD and returns the index of the action in ORDERED_ACTION_LIST."""
|
|
30
|
+
bdd = PACKED_BDD
|
|
31
|
+
fns = STATE_RESOLVE_FUNCTION_BY_INDEX
|
|
32
|
+
n = _NODE_COUNT
|
|
33
|
+
i = 0
|
|
34
|
+
while True:
|
|
35
|
+
ref = bdd[i + 2] if fns[bdd[i]](input) else bdd[i + 1]
|
|
36
|
+
if ref >= n:
|
|
37
|
+
return ref - n
|
|
38
|
+
i = ref * 3
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Code generated by the truth-table-generator of the JavaScript implementation. DO NOT EDIT.
|
|
2
|
+
# Run `npm run optimize-bdd` in ./javascript instead.
|
|
3
|
+
|
|
4
|
+
PACKED_BDD: tuple[int, ...] = (1, 1, 2, 0, 3, 4, 17, 5, 6, 6, 7, 92, 17, 92, 8, 6, 9, 92, 6, 10, 11, 11, 12, 13, 6, 14, 15, 11, 16, 13, 11, 17, 18, 5, 93, 19, 5, 92, 20, 7, 21, 108, 5, 22, 23, 5, 93, 108, 5, 92, 24, 5, 22, 25, 5, 26, 27, 16, 108, 92, 12, 92, 28, 8, 29, 95, 7, 30, 31, 7, 32, 33, 10, 34, 20, 10, 35, 36, 10, 37, 38, 10, 37, 39, 7, 40, 41, 9, 101, 96, 15, 42, 94, 15, 43, 92, 15, 44, 94, 15, 45, 92, 7, 46, 47, 7, 48, 49, 12, 23, 50, 8, 51, 52, 7, 53, 54, 7, 55, 56, 16, 92, 95, 16, 92, 108, 14, 104, 93, 4, 57, 97, 14, 104, 108, 4, 58, 108, 15, 59, 92, 15, 60, 92, 15, 61, 94, 15, 62, 92, 7, 63, 64, 9, 102, 100, 9, 99, 97, 8, 65, 66, 8, 67, 68, 8, 69, 70, 8, 71, 72, 14, 106, 98, 14, 106, 108, 14, 92, 40, 4, 73, 41, 14, 104, 19, 4, 74, 19, 15, 75, 76, 15, 77, 41, 9, 105, 78, 9, 79, 97, 9, 80, 81, 9, 82, 108, 9, 83, 84, 9, 85, 108, 15, 86, 108, 9, 71, 108, 14, 92, 41, 14, 106, 19, 14, 87, 19, 16, 94, 97, 4, 88, 19, 15, 89, 100, 15, 90, 97, 15, 105, 108, 15, 89, 108, 15, 90, 108, 15, 86, 105, 15, 86, 100, 15, 86, 97, 14, 105, 108, 16, 104, 107, 14, 91, 19, 14, 105, 98, 14, 105, 99, 16, 106, 107) # fmt: skip
|
|
File without changes
|