zlmdb 25.10.1__cp312-cp312-win_amd64.whl
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.
Potentially problematic release.
This version of zlmdb might be problematic. Click here for more details.
- flatbuffers/__init__.py +19 -0
- flatbuffers/_version.py +17 -0
- flatbuffers/builder.py +776 -0
- flatbuffers/compat.py +86 -0
- flatbuffers/encode.py +42 -0
- flatbuffers/flexbuffers.py +1527 -0
- flatbuffers/number_types.py +181 -0
- flatbuffers/packer.py +42 -0
- flatbuffers/reflection/AdvancedFeatures.py +10 -0
- flatbuffers/reflection/BaseType.py +24 -0
- flatbuffers/reflection/Enum.py +169 -0
- flatbuffers/reflection/EnumVal.py +96 -0
- flatbuffers/reflection/Field.py +208 -0
- flatbuffers/reflection/KeyValue.py +56 -0
- flatbuffers/reflection/Object.py +175 -0
- flatbuffers/reflection/RPCCall.py +131 -0
- flatbuffers/reflection/Schema.py +206 -0
- flatbuffers/reflection/SchemaFile.py +77 -0
- flatbuffers/reflection/Service.py +145 -0
- flatbuffers/reflection/Type.py +98 -0
- flatbuffers/reflection/__init__.py +0 -0
- flatbuffers/table.py +129 -0
- flatbuffers/util.py +43 -0
- zlmdb/__init__.py +312 -0
- zlmdb/_database.py +990 -0
- zlmdb/_errors.py +31 -0
- zlmdb/_meta.py +27 -0
- zlmdb/_pmap.py +1667 -0
- zlmdb/_schema.py +137 -0
- zlmdb/_transaction.py +181 -0
- zlmdb/_types.py +1596 -0
- zlmdb/_version.py +27 -0
- zlmdb/cli.py +41 -0
- zlmdb/flatbuffers/__init__.py +5 -0
- zlmdb/flatbuffers/reflection/AdvancedFeatures.py +10 -0
- zlmdb/flatbuffers/reflection/BaseType.py +25 -0
- zlmdb/flatbuffers/reflection/Enum.py +252 -0
- zlmdb/flatbuffers/reflection/EnumVal.py +144 -0
- zlmdb/flatbuffers/reflection/Field.py +325 -0
- zlmdb/flatbuffers/reflection/KeyValue.py +84 -0
- zlmdb/flatbuffers/reflection/Object.py +260 -0
- zlmdb/flatbuffers/reflection/RPCCall.py +195 -0
- zlmdb/flatbuffers/reflection/Schema.py +301 -0
- zlmdb/flatbuffers/reflection/SchemaFile.py +112 -0
- zlmdb/flatbuffers/reflection/Service.py +213 -0
- zlmdb/flatbuffers/reflection/Type.py +148 -0
- zlmdb/flatbuffers/reflection/__init__.py +0 -0
- zlmdb/flatbuffers/reflection.fbs +152 -0
- zlmdb/lmdb/__init__.py +37 -0
- zlmdb/lmdb/__main__.py +25 -0
- zlmdb/lmdb/_config.py +10 -0
- zlmdb/lmdb/cffi.py +2606 -0
- zlmdb/lmdb/tool.py +670 -0
- zlmdb/tests/lmdb/__init__.py +0 -0
- zlmdb/tests/lmdb/address_book.py +287 -0
- zlmdb/tests/lmdb/crash_test.py +339 -0
- zlmdb/tests/lmdb/cursor_test.py +333 -0
- zlmdb/tests/lmdb/env_test.py +919 -0
- zlmdb/tests/lmdb/getmulti_test.py +92 -0
- zlmdb/tests/lmdb/iteration_test.py +258 -0
- zlmdb/tests/lmdb/package_test.py +70 -0
- zlmdb/tests/lmdb/test_lmdb.py +188 -0
- zlmdb/tests/lmdb/testlib.py +185 -0
- zlmdb/tests/lmdb/tool_test.py +60 -0
- zlmdb/tests/lmdb/txn_test.py +575 -0
- zlmdb/tests/orm/MNodeLog.py +853 -0
- zlmdb/tests/orm/__init__.py +0 -0
- zlmdb/tests/orm/_schema_fbs.py +215 -0
- zlmdb/tests/orm/_schema_mnode_log.py +1201 -0
- zlmdb/tests/orm/_schema_py2.py +250 -0
- zlmdb/tests/orm/_schema_py3.py +307 -0
- zlmdb/tests/orm/_test_flatbuffers.py +144 -0
- zlmdb/tests/orm/_test_serialization.py +144 -0
- zlmdb/tests/orm/test_basic.py +217 -0
- zlmdb/tests/orm/test_etcd.py +275 -0
- zlmdb/tests/orm/test_pmap_indexes.py +466 -0
- zlmdb/tests/orm/test_pmap_types.py +90 -0
- zlmdb/tests/orm/test_pmaps.py +295 -0
- zlmdb/tests/orm/test_select.py +619 -0
- zlmdb-25.10.1.dist-info/METADATA +264 -0
- zlmdb-25.10.1.dist-info/RECORD +86 -0
- zlmdb-25.10.1.dist-info/WHEEL +5 -0
- zlmdb-25.10.1.dist-info/entry_points.txt +2 -0
- zlmdb-25.10.1.dist-info/licenses/LICENSE +137 -0
- zlmdb-25.10.1.dist-info/licenses/NOTICE +41 -0
- zlmdb-25.10.1.dist-info/top_level.txt +2 -0
zlmdb/_database.py
ADDED
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
#############################################################################
|
|
2
|
+
#
|
|
3
|
+
# The MIT License (MIT)
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) typedef int GmbH
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
# THE SOFTWARE.
|
|
24
|
+
#
|
|
25
|
+
###############################################################################
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import tempfile
|
|
30
|
+
import uuid
|
|
31
|
+
import pprint
|
|
32
|
+
import struct
|
|
33
|
+
import inspect
|
|
34
|
+
import time
|
|
35
|
+
from typing import Dict, Any, Tuple, List, Optional, Callable, Type
|
|
36
|
+
|
|
37
|
+
import zlmdb.lmdb as lmdb
|
|
38
|
+
import yaml
|
|
39
|
+
import cbor2
|
|
40
|
+
|
|
41
|
+
from zlmdb._transaction import Transaction, TransactionStats
|
|
42
|
+
from zlmdb import _pmap
|
|
43
|
+
from zlmdb._pmap import MapStringJson, MapStringCbor, MapUuidJson, MapUuidCbor
|
|
44
|
+
|
|
45
|
+
import txaio
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from twisted.python.reflect import qual
|
|
49
|
+
except ImportError:
|
|
50
|
+
|
|
51
|
+
def qual(clazz: type) -> str: # type: ignore[misc]
|
|
52
|
+
return clazz.__name__
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
KV_TYPE_TO_CLASS = {
|
|
56
|
+
"string-json": (MapStringJson, lambda x: x, lambda x: x),
|
|
57
|
+
"string-cbor": (MapStringCbor, lambda x: x, lambda x: x),
|
|
58
|
+
"uuid-json": (MapUuidJson, lambda x: x, lambda x: x),
|
|
59
|
+
"uuid-cbor": (MapUuidCbor, lambda x: x, lambda x: x),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_LMDB_MYPID_ENVS: Dict[str, Tuple["Database", int]] = {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ConfigurationElement(object):
|
|
66
|
+
"""
|
|
67
|
+
Internal zLMDB configuration element base type.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = (
|
|
71
|
+
"_oid",
|
|
72
|
+
"_name",
|
|
73
|
+
"_description",
|
|
74
|
+
"_tags",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
oid: Optional[uuid.UUID] = None,
|
|
80
|
+
name: Optional[str] = None,
|
|
81
|
+
description: Optional[str] = None,
|
|
82
|
+
tags: Optional[List[str]] = None,
|
|
83
|
+
):
|
|
84
|
+
self._oid = oid
|
|
85
|
+
self._name = name
|
|
86
|
+
self._description = description
|
|
87
|
+
self._tags = tags
|
|
88
|
+
|
|
89
|
+
def __eq__(self, other: Any) -> bool:
|
|
90
|
+
if not isinstance(other, self.__class__):
|
|
91
|
+
return False
|
|
92
|
+
if other.oid != self.oid:
|
|
93
|
+
return False
|
|
94
|
+
if other.name != self.name:
|
|
95
|
+
return False
|
|
96
|
+
if other.description != self.description:
|
|
97
|
+
return False
|
|
98
|
+
if (self.tags and not other.tags) or (not self.tags and other.tags):
|
|
99
|
+
return False
|
|
100
|
+
if other.tags and self.tags:
|
|
101
|
+
if set(other.tags) ^ set(self.tags):
|
|
102
|
+
return False
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def __ne__(self, other: Any) -> bool:
|
|
106
|
+
return not self.__eq__(other)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def oid(self) -> Optional[uuid.UUID]:
|
|
110
|
+
return self._oid
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def name(self) -> Optional[str]:
|
|
114
|
+
return self._name
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def description(self) -> Optional[str]:
|
|
118
|
+
return self._description
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def tags(self) -> Optional[List[str]]:
|
|
122
|
+
return self._tags
|
|
123
|
+
|
|
124
|
+
def __str__(self) -> str:
|
|
125
|
+
return pprint.pformat(self.marshal())
|
|
126
|
+
|
|
127
|
+
def marshal(self) -> Dict[str, Any]:
|
|
128
|
+
value: Dict[str, Any] = {
|
|
129
|
+
"oid": str(self._oid),
|
|
130
|
+
"name": self._name,
|
|
131
|
+
}
|
|
132
|
+
if self.description:
|
|
133
|
+
value["description"] = self._description
|
|
134
|
+
if self.tags:
|
|
135
|
+
value["tags"] = self._tags
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def parse(value: Dict[str, Any]) -> "ConfigurationElement":
|
|
140
|
+
assert type(value) == dict
|
|
141
|
+
oid = value.get("oid", None)
|
|
142
|
+
if oid:
|
|
143
|
+
oid = uuid.UUID(oid)
|
|
144
|
+
obj = ConfigurationElement(
|
|
145
|
+
oid=oid,
|
|
146
|
+
name=value.get("name", None),
|
|
147
|
+
description=value.get("description", None),
|
|
148
|
+
tags=value.get("tags", None),
|
|
149
|
+
)
|
|
150
|
+
return obj
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Slot(ConfigurationElement):
|
|
154
|
+
"""
|
|
155
|
+
Internal zLMDB database slot configuration element.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
__slots__ = (
|
|
159
|
+
"_slot",
|
|
160
|
+
"_creator",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
oid: Optional[uuid.UUID] = None,
|
|
166
|
+
name: Optional[str] = None,
|
|
167
|
+
description: Optional[str] = None,
|
|
168
|
+
tags: Optional[List[str]] = None,
|
|
169
|
+
slot: Optional[int] = None,
|
|
170
|
+
creator: Optional[str] = None,
|
|
171
|
+
):
|
|
172
|
+
ConfigurationElement.__init__(
|
|
173
|
+
self, oid=oid, name=name, description=description, tags=tags
|
|
174
|
+
)
|
|
175
|
+
self._slot = slot
|
|
176
|
+
self._creator = creator
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def creator(self) -> Optional[str]:
|
|
180
|
+
return self._creator
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def slot(self) -> Optional[int]:
|
|
184
|
+
return self._slot
|
|
185
|
+
|
|
186
|
+
def __str__(self) -> str:
|
|
187
|
+
return pprint.pformat(self.marshal())
|
|
188
|
+
|
|
189
|
+
def marshal(self) -> Dict[str, Any]:
|
|
190
|
+
obj = ConfigurationElement.marshal(self)
|
|
191
|
+
obj.update(
|
|
192
|
+
{
|
|
193
|
+
"creator": self._creator,
|
|
194
|
+
"slot": self._slot,
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
return obj
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def parse(data: Dict[str, Any]) -> "Slot":
|
|
201
|
+
assert type(data) == dict
|
|
202
|
+
|
|
203
|
+
obj = ConfigurationElement.parse(data)
|
|
204
|
+
|
|
205
|
+
slot = data.get("slot", None)
|
|
206
|
+
creator = data.get("creator", None)
|
|
207
|
+
|
|
208
|
+
drvd_obj = Slot(
|
|
209
|
+
oid=obj.oid,
|
|
210
|
+
name=obj.name,
|
|
211
|
+
description=obj.description,
|
|
212
|
+
tags=obj.tags,
|
|
213
|
+
slot=slot,
|
|
214
|
+
creator=creator,
|
|
215
|
+
)
|
|
216
|
+
return drvd_obj
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Schema(object):
|
|
220
|
+
def __init__(self, meta, slots, slots_byname):
|
|
221
|
+
self._meta = meta
|
|
222
|
+
self._slots = slots
|
|
223
|
+
self._slots_byname = slots_byname
|
|
224
|
+
|
|
225
|
+
def __str__(self):
|
|
226
|
+
return pprint.pformat(self._meta)
|
|
227
|
+
|
|
228
|
+
def __getitem__(self, name):
|
|
229
|
+
assert type(name) == str
|
|
230
|
+
|
|
231
|
+
if name not in self._slots_byname:
|
|
232
|
+
raise IndexError('no slot with name "{}"'.format(name))
|
|
233
|
+
|
|
234
|
+
return self._slots[self._slots_byname[name]]
|
|
235
|
+
|
|
236
|
+
def __setitem__(self, name, value):
|
|
237
|
+
raise NotImplementedError("schema is read-only")
|
|
238
|
+
|
|
239
|
+
def __delitem__(self, name):
|
|
240
|
+
raise NotImplementedError("schema is read-only")
|
|
241
|
+
|
|
242
|
+
def __len__(self):
|
|
243
|
+
return len(self._slots_byname)
|
|
244
|
+
|
|
245
|
+
def __iter__(self):
|
|
246
|
+
raise Exception("not implemented")
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def parse(filename, klassmap=KV_TYPE_TO_CLASS):
|
|
250
|
+
with open(filename) as f:
|
|
251
|
+
_meta = yaml.safe_load(f.read())
|
|
252
|
+
|
|
253
|
+
meta = {}
|
|
254
|
+
slots = {}
|
|
255
|
+
slots_byname = {}
|
|
256
|
+
|
|
257
|
+
for slot in _meta.get("slots", []):
|
|
258
|
+
_index = slot.get("index", None)
|
|
259
|
+
assert type(_index) == int and _index >= 100 and _index < 65536
|
|
260
|
+
assert _index not in slots
|
|
261
|
+
|
|
262
|
+
_name = slot.get("name", None)
|
|
263
|
+
assert type(_name) == str
|
|
264
|
+
assert _name not in slots_byname
|
|
265
|
+
|
|
266
|
+
_key = slot.get("key", None)
|
|
267
|
+
assert _key in ["string", "uuid"]
|
|
268
|
+
|
|
269
|
+
_value = slot.get("value", None)
|
|
270
|
+
assert _value in ["json", "cbor"]
|
|
271
|
+
|
|
272
|
+
_schema = slot.get("schema", None)
|
|
273
|
+
assert _schema is None or type(_schema) == str
|
|
274
|
+
|
|
275
|
+
_description = slot.get("description", None)
|
|
276
|
+
assert _description is None or type(_description) == str
|
|
277
|
+
|
|
278
|
+
if _schema:
|
|
279
|
+
_kv_type = "{}-{}-{}".format(_key, _value, _schema)
|
|
280
|
+
else:
|
|
281
|
+
_kv_type = "{}-{}".format(_key, _value)
|
|
282
|
+
|
|
283
|
+
_kv_klass, _marshal, _unmarshal = klassmap.get(
|
|
284
|
+
_kv_type, (None, None, None)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
assert _kv_klass
|
|
288
|
+
assert _marshal
|
|
289
|
+
assert _unmarshal
|
|
290
|
+
|
|
291
|
+
meta[_index] = {
|
|
292
|
+
"index": _index,
|
|
293
|
+
"name": _name,
|
|
294
|
+
"key": _key,
|
|
295
|
+
"value": _value,
|
|
296
|
+
"impl": _kv_klass.__name__ if _kv_klass else None,
|
|
297
|
+
"description": _description,
|
|
298
|
+
}
|
|
299
|
+
slots[_index] = _kv_klass(
|
|
300
|
+
_index, marshal=_marshal, unmarshal=_unmarshal
|
|
301
|
+
)
|
|
302
|
+
slots_byname[_name] = _index
|
|
303
|
+
|
|
304
|
+
return Schema(meta, slots, slots_byname)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class Database(object):
|
|
308
|
+
"""
|
|
309
|
+
ZLMDB database access.
|
|
310
|
+
|
|
311
|
+
Objects of this class are generally "light-weight" (cheap to create and
|
|
312
|
+
destroy), but do manage internal resource such as file descriptors.
|
|
313
|
+
|
|
314
|
+
To manage these resources in a robust way, this class implements
|
|
315
|
+
the Python context manager interface.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
__slots__ = (
|
|
319
|
+
"log",
|
|
320
|
+
"_is_temp",
|
|
321
|
+
"_tempdir",
|
|
322
|
+
"_dbpath",
|
|
323
|
+
"_maxsize",
|
|
324
|
+
"_readonly",
|
|
325
|
+
"_lock",
|
|
326
|
+
"_sync",
|
|
327
|
+
"_create",
|
|
328
|
+
"_open_now",
|
|
329
|
+
"_writemap",
|
|
330
|
+
"_context",
|
|
331
|
+
"_slots",
|
|
332
|
+
"_slots_by_index",
|
|
333
|
+
"_env",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def __init__(
|
|
337
|
+
self,
|
|
338
|
+
dbpath: Optional[str] = None,
|
|
339
|
+
maxsize: int = 10485760,
|
|
340
|
+
readonly: bool = False,
|
|
341
|
+
lock: bool = True,
|
|
342
|
+
sync: bool = True,
|
|
343
|
+
create: bool = True,
|
|
344
|
+
open_now: bool = True,
|
|
345
|
+
writemap: bool = False,
|
|
346
|
+
context: Any = None,
|
|
347
|
+
log: Optional[txaio.interfaces.ILogger] = None,
|
|
348
|
+
):
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
:param dbpath: LMDB database path: a directory with (at least) 2 files, a ``data.mdb`` and a ``lock.mdb``.
|
|
352
|
+
If no database exists at the given path, create a new one.
|
|
353
|
+
:param maxsize: Database size limit in bytes, with a default of 1MB.
|
|
354
|
+
:param readonly: Open database read-only. When ``True``, deny any modifying database operations.
|
|
355
|
+
Note that the LMDB lock file (``lock.mdb``) still needs to be written (by readers also),
|
|
356
|
+
and hence at the filesystem level, a LMDB database directory must be writable.
|
|
357
|
+
:param sync: Open database with sync on commit.
|
|
358
|
+
:param create: Automatically create database if it does not yet exist.
|
|
359
|
+
:param open_now: Open the database immediately (within this constructor).
|
|
360
|
+
:param writemap: Use direct write to mmap'ed database rather than regular file IO writes. Be careful when
|
|
361
|
+
using any storage other than locally attached filesystem/drive.
|
|
362
|
+
:param context: Optional context within which this database instance is created.
|
|
363
|
+
:param log: Log object to use for logging from this class.
|
|
364
|
+
"""
|
|
365
|
+
self._context = context
|
|
366
|
+
|
|
367
|
+
if log:
|
|
368
|
+
self.log = log
|
|
369
|
+
else:
|
|
370
|
+
if not txaio._explicit_framework:
|
|
371
|
+
txaio.use_asyncio()
|
|
372
|
+
self.log = txaio.make_logger()
|
|
373
|
+
|
|
374
|
+
if dbpath:
|
|
375
|
+
self._is_temp = False
|
|
376
|
+
self._tempdir = None
|
|
377
|
+
self._dbpath = dbpath
|
|
378
|
+
else:
|
|
379
|
+
self._is_temp = True
|
|
380
|
+
self._tempdir = tempfile.TemporaryDirectory()
|
|
381
|
+
self._dbpath = self._tempdir.name
|
|
382
|
+
|
|
383
|
+
self._maxsize = maxsize
|
|
384
|
+
self._readonly = readonly
|
|
385
|
+
self._lock = lock
|
|
386
|
+
self._sync = sync
|
|
387
|
+
self._create = create
|
|
388
|
+
self._open_now = open_now
|
|
389
|
+
self._writemap = writemap
|
|
390
|
+
self._context = context
|
|
391
|
+
|
|
392
|
+
self._slots: Optional[Dict[uuid.UUID, Slot]] = None
|
|
393
|
+
self._slots_by_index: Optional[Dict[uuid.UUID, int]] = None
|
|
394
|
+
|
|
395
|
+
# in a context manager environment we initialize with LMDB handle
|
|
396
|
+
# when we enter the actual temporary, managed context ..
|
|
397
|
+
self._env: Optional[lmdb.Environment] = None
|
|
398
|
+
|
|
399
|
+
# in a direct run environment, we immediately open LMDB
|
|
400
|
+
if self._open_now:
|
|
401
|
+
self.__enter__()
|
|
402
|
+
|
|
403
|
+
def __enter__(self):
|
|
404
|
+
"""
|
|
405
|
+
Enter database runtime context and open the underlying LMDB database environment.
|
|
406
|
+
|
|
407
|
+
.. note::
|
|
408
|
+
|
|
409
|
+
Enter the runtime context related to this object. The with statement will bind this method’s
|
|
410
|
+
return value to the target(s) specified in the as clause of the statement, if any.
|
|
411
|
+
[Source](https://docs.python.org/3/reference/datamodel.html#object.__enter__)
|
|
412
|
+
|
|
413
|
+
.. note::
|
|
414
|
+
|
|
415
|
+
A context manager is an object that defines the runtime context to be established when
|
|
416
|
+
executing a with statement. The context manager handles the entry into, and the exit from,
|
|
417
|
+
the desired runtime context for the execution of the block of code. Context managers are
|
|
418
|
+
normally invoked using the with statement (described in section The with statement), but
|
|
419
|
+
can also be used by directly invoking their methods."
|
|
420
|
+
[Source](https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers)
|
|
421
|
+
|
|
422
|
+
:return: This database instance (in open state).
|
|
423
|
+
"""
|
|
424
|
+
if not self._env:
|
|
425
|
+
# protect against opening the same database file multiple times within the same process:
|
|
426
|
+
# "It is a serious error to have open (multiple times) the same LMDB file in
|
|
427
|
+
# the same process at the same time. Failure to heed this may lead to data
|
|
428
|
+
# corruption and interpreter crash."
|
|
429
|
+
# https://lmdb.readthedocs.io/en/release/#environment-class
|
|
430
|
+
|
|
431
|
+
if not self._is_temp:
|
|
432
|
+
if self._dbpath in _LMDB_MYPID_ENVS:
|
|
433
|
+
other_obj, other_pid = _LMDB_MYPID_ENVS[self._dbpath]
|
|
434
|
+
raise RuntimeError(
|
|
435
|
+
'tried to open same dbpath "{}" twice within same process: cannot open database '
|
|
436
|
+
"for {} (PID {}, Context {}), already opened in {} (PID {}, Context {})".format(
|
|
437
|
+
self._dbpath,
|
|
438
|
+
self,
|
|
439
|
+
os.getpid(),
|
|
440
|
+
self.context,
|
|
441
|
+
other_obj,
|
|
442
|
+
other_pid,
|
|
443
|
+
other_obj.context,
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
_LMDB_MYPID_ENVS[self._dbpath] = self, os.getpid()
|
|
447
|
+
|
|
448
|
+
# handle lmdb.LockError: mdb_txn_begin: Resource temporarily unavailable
|
|
449
|
+
# "The environment was locked by another process."
|
|
450
|
+
# https://lmdb.readthedocs.io/en/release/#lmdb.LockError
|
|
451
|
+
|
|
452
|
+
# count number of retries
|
|
453
|
+
retries = 0
|
|
454
|
+
# delay (in seconds) before retrying
|
|
455
|
+
retry_delay = 0
|
|
456
|
+
while True:
|
|
457
|
+
try:
|
|
458
|
+
# https://lmdb.readthedocs.io/en/release/#lmdb.Environment
|
|
459
|
+
# https://lmdb.readthedocs.io/en/release/#writemap-mode
|
|
460
|
+
# map_size: Maximum size database may grow to; used to size the memory mapping.
|
|
461
|
+
# lock=True is needed for concurrent access, even when only by readers (because of space mgmt)
|
|
462
|
+
self._env = lmdb.open(
|
|
463
|
+
self._dbpath,
|
|
464
|
+
map_size=self._maxsize,
|
|
465
|
+
create=self._create,
|
|
466
|
+
readonly=self._readonly,
|
|
467
|
+
sync=self._sync,
|
|
468
|
+
subdir=True,
|
|
469
|
+
lock=self._lock,
|
|
470
|
+
writemap=self._writemap,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# ok, good: we've got a LMDB env
|
|
474
|
+
break
|
|
475
|
+
|
|
476
|
+
# see https://github.com/crossbario/zlmdb/issues/53
|
|
477
|
+
except lmdb.LockError as e:
|
|
478
|
+
retries += 1
|
|
479
|
+
if retries >= 3:
|
|
480
|
+
# give up and signal to user code
|
|
481
|
+
raise RuntimeError(
|
|
482
|
+
"cannot open LMDB environment (giving up "
|
|
483
|
+
"after {} retries): {}".format(retries, e)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# use synchronous (!) sleep (1st time is sleep(0), which releases execution of this process to OS)
|
|
487
|
+
time.sleep(retry_delay)
|
|
488
|
+
|
|
489
|
+
# increase sleep time by 10ms _next_ time. that is, for our 3 attempts
|
|
490
|
+
# the delays are: 0ms, 10ms, 20ms
|
|
491
|
+
retry_delay += 0.01
|
|
492
|
+
|
|
493
|
+
return self
|
|
494
|
+
|
|
495
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
496
|
+
"""
|
|
497
|
+
Exit runtime context and close the underlying LMDB database environment.
|
|
498
|
+
|
|
499
|
+
.. note::
|
|
500
|
+
|
|
501
|
+
Exit the runtime context related to this object. The parameters describe the exception that
|
|
502
|
+
caused the context to be exited. If the context was exited without an exception, all three
|
|
503
|
+
arguments will be None.
|
|
504
|
+
[Source](https://docs.python.org/3/reference/datamodel.html#object.__exit__).
|
|
505
|
+
|
|
506
|
+
:param exc_type:
|
|
507
|
+
:param exc_value:
|
|
508
|
+
:param traceback:
|
|
509
|
+
:return:
|
|
510
|
+
"""
|
|
511
|
+
if self._env:
|
|
512
|
+
self._env.close()
|
|
513
|
+
self._env = None
|
|
514
|
+
if not self._is_temp and self._dbpath in _LMDB_MYPID_ENVS:
|
|
515
|
+
del _LMDB_MYPID_ENVS[self._dbpath]
|
|
516
|
+
|
|
517
|
+
@staticmethod
|
|
518
|
+
def open(
|
|
519
|
+
dbpath: Optional[str] = None,
|
|
520
|
+
maxsize: int = 10485760,
|
|
521
|
+
readonly: bool = False,
|
|
522
|
+
lock: bool = True,
|
|
523
|
+
sync: bool = True,
|
|
524
|
+
create: bool = True,
|
|
525
|
+
open_now: bool = True,
|
|
526
|
+
writemap: bool = False,
|
|
527
|
+
context: Any = None,
|
|
528
|
+
log: Optional[txaio.interfaces.ILogger] = None,
|
|
529
|
+
) -> "Database":
|
|
530
|
+
if dbpath is not None and dbpath in _LMDB_MYPID_ENVS:
|
|
531
|
+
db, _ = _LMDB_MYPID_ENVS[dbpath]
|
|
532
|
+
print(
|
|
533
|
+
'{}: reusing database instance for path "{}" in new context {} already opened from (first) context {}'.format(
|
|
534
|
+
Database.open, dbpath, context, db.context
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
|
+
db = Database(
|
|
539
|
+
dbpath=dbpath,
|
|
540
|
+
maxsize=maxsize,
|
|
541
|
+
readonly=readonly,
|
|
542
|
+
lock=lock,
|
|
543
|
+
sync=sync,
|
|
544
|
+
create=create,
|
|
545
|
+
open_now=open_now,
|
|
546
|
+
writemap=writemap,
|
|
547
|
+
context=context,
|
|
548
|
+
log=log,
|
|
549
|
+
)
|
|
550
|
+
print(
|
|
551
|
+
'{}: creating new database instance for path "{}" in context {}'.format(
|
|
552
|
+
Database.open, dbpath, context
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
return db
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def context(self):
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
:return:
|
|
562
|
+
"""
|
|
563
|
+
return self._context
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def dbpath(self) -> Optional[str]:
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
:return:
|
|
570
|
+
"""
|
|
571
|
+
return self._dbpath
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def maxsize(self) -> int:
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
:return:
|
|
578
|
+
"""
|
|
579
|
+
return self._maxsize
|
|
580
|
+
|
|
581
|
+
@property
|
|
582
|
+
def is_sync(self) -> bool:
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
:return:
|
|
586
|
+
"""
|
|
587
|
+
return self._sync
|
|
588
|
+
|
|
589
|
+
@property
|
|
590
|
+
def is_readonly(self) -> bool:
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
:return:
|
|
594
|
+
"""
|
|
595
|
+
return self._readonly
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def is_writemap(self) -> bool:
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
:return:
|
|
602
|
+
"""
|
|
603
|
+
return self._writemap
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def is_open(self) -> bool:
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
:return:
|
|
610
|
+
"""
|
|
611
|
+
return self._env is not None
|
|
612
|
+
|
|
613
|
+
@staticmethod
|
|
614
|
+
def scratch(dbpath: str):
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
:param dbpath:
|
|
618
|
+
:return:
|
|
619
|
+
"""
|
|
620
|
+
if os.path.exists(dbpath):
|
|
621
|
+
if os.path.isdir(dbpath):
|
|
622
|
+
shutil.rmtree(dbpath)
|
|
623
|
+
else:
|
|
624
|
+
os.remove(dbpath)
|
|
625
|
+
|
|
626
|
+
def begin(
|
|
627
|
+
self,
|
|
628
|
+
write: bool = False,
|
|
629
|
+
buffers: bool = False,
|
|
630
|
+
stats: Optional[TransactionStats] = None,
|
|
631
|
+
) -> Transaction:
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
:param write:
|
|
635
|
+
:param buffers:
|
|
636
|
+
:param stats:
|
|
637
|
+
:return:
|
|
638
|
+
"""
|
|
639
|
+
assert self._env is not None
|
|
640
|
+
|
|
641
|
+
if write and self._readonly:
|
|
642
|
+
raise Exception("database is read-only")
|
|
643
|
+
|
|
644
|
+
txn = Transaction(db=self, write=write, buffers=buffers, stats=stats)
|
|
645
|
+
return txn
|
|
646
|
+
|
|
647
|
+
def sync(self, force: bool = False):
|
|
648
|
+
"""
|
|
649
|
+
|
|
650
|
+
:param force:
|
|
651
|
+
:return:
|
|
652
|
+
"""
|
|
653
|
+
assert self._env is not None
|
|
654
|
+
|
|
655
|
+
self._env.sync(force=force)
|
|
656
|
+
|
|
657
|
+
def config(self) -> Dict[str, Any]:
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
:return:
|
|
661
|
+
"""
|
|
662
|
+
res = {
|
|
663
|
+
"is_temp": self._is_temp,
|
|
664
|
+
"dbpath": self._dbpath,
|
|
665
|
+
"maxsize": self._maxsize,
|
|
666
|
+
"readonly": self._readonly,
|
|
667
|
+
"lock": self._lock,
|
|
668
|
+
"sync": self._sync,
|
|
669
|
+
"create": self._create,
|
|
670
|
+
"open_now": self._open_now,
|
|
671
|
+
"writemap": self._writemap,
|
|
672
|
+
"context": str(self._context) if self._context else None,
|
|
673
|
+
}
|
|
674
|
+
return res
|
|
675
|
+
|
|
676
|
+
def stats(self, include_slots: bool = False) -> Dict[str, Any]:
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
:param include_slots:
|
|
680
|
+
:return:
|
|
681
|
+
"""
|
|
682
|
+
assert self._env is not None
|
|
683
|
+
|
|
684
|
+
current_size = os.path.getsize(os.path.join(self._dbpath, "data.mdb"))
|
|
685
|
+
|
|
686
|
+
# psize Size of a database page in bytes.
|
|
687
|
+
# depth Height of the B-tree.
|
|
688
|
+
# branch_pages Number of internal (non-leaf) pages.
|
|
689
|
+
# leaf_pages Number of leaf pages.
|
|
690
|
+
# overflow_pages Number of overflow pages.
|
|
691
|
+
# entries Number of data items.
|
|
692
|
+
stats = self._env.stat()
|
|
693
|
+
pages = stats["leaf_pages"] + stats["overflow_pages"] + stats["branch_pages"]
|
|
694
|
+
used = stats["psize"] * pages
|
|
695
|
+
|
|
696
|
+
self._cache_slots()
|
|
697
|
+
res: Dict[str, Any] = {
|
|
698
|
+
"num_slots": len(self._slots) if self._slots else 0,
|
|
699
|
+
"current_size": current_size,
|
|
700
|
+
"max_size": self._maxsize,
|
|
701
|
+
"page_size": stats["psize"],
|
|
702
|
+
"pages": pages,
|
|
703
|
+
"used": used,
|
|
704
|
+
"free": 1.0 - float(used) / float(self._maxsize),
|
|
705
|
+
"read_only": self._readonly,
|
|
706
|
+
"sync_enabled": self._sync,
|
|
707
|
+
}
|
|
708
|
+
res.update(stats)
|
|
709
|
+
|
|
710
|
+
# map_addr Address of database map in RAM.
|
|
711
|
+
# map_size Size of database map in RAM.
|
|
712
|
+
# last_pgno ID of last used page.
|
|
713
|
+
# last_txnid ID of last committed transaction.
|
|
714
|
+
# max_readers Number of reader slots allocated in the lock file. Equivalent to the value of
|
|
715
|
+
# maxreaders= specified by the first process opening the Environment.
|
|
716
|
+
# num_readers Maximum number of reader slots in simultaneous use since the lock file was initialized.
|
|
717
|
+
res.update(self._env.info())
|
|
718
|
+
|
|
719
|
+
if include_slots:
|
|
720
|
+
slots = self._get_slots()
|
|
721
|
+
res["slots"] = []
|
|
722
|
+
with self.begin() as txn:
|
|
723
|
+
for slot_id in slots:
|
|
724
|
+
slot = slots[slot_id]
|
|
725
|
+
pmap = _pmap.PersistentMap(slot.slot)
|
|
726
|
+
res["slots"].append(
|
|
727
|
+
{
|
|
728
|
+
"oid": str(slot_id),
|
|
729
|
+
"slot": slot.slot,
|
|
730
|
+
"name": slot.name,
|
|
731
|
+
"description": slot.description,
|
|
732
|
+
"records": pmap.count(txn),
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
return res
|
|
737
|
+
|
|
738
|
+
def _cache_slots(self):
|
|
739
|
+
"""
|
|
740
|
+
|
|
741
|
+
:return:
|
|
742
|
+
"""
|
|
743
|
+
slots = {}
|
|
744
|
+
slots_by_index = {}
|
|
745
|
+
|
|
746
|
+
with self.begin() as txn:
|
|
747
|
+
from_key = struct.pack(">H", 0)
|
|
748
|
+
to_key = struct.pack(">H", 1)
|
|
749
|
+
|
|
750
|
+
cursor = txn._txn.cursor()
|
|
751
|
+
found = cursor.set_range(from_key)
|
|
752
|
+
while found:
|
|
753
|
+
_key = cursor.key()
|
|
754
|
+
if _key >= to_key:
|
|
755
|
+
break
|
|
756
|
+
|
|
757
|
+
if len(_key) >= 4:
|
|
758
|
+
# key = struct.unpack('>H', _key[0:2])
|
|
759
|
+
slot_index = struct.unpack(">H", _key[2:4])[0]
|
|
760
|
+
slot = Slot.parse(cbor2.loads(cursor.value()))
|
|
761
|
+
assert slot.slot == slot_index
|
|
762
|
+
slots[slot.oid] = slot
|
|
763
|
+
slots_by_index[slot.oid] = slot_index
|
|
764
|
+
|
|
765
|
+
found = cursor.next()
|
|
766
|
+
|
|
767
|
+
self._slots = slots
|
|
768
|
+
self._slots_by_index = slots_by_index
|
|
769
|
+
|
|
770
|
+
def _get_slots(self, cached=True) -> Dict[uuid.UUID, Slot]:
|
|
771
|
+
"""
|
|
772
|
+
|
|
773
|
+
:param cached:
|
|
774
|
+
:return:
|
|
775
|
+
"""
|
|
776
|
+
if self._slots is None or not cached:
|
|
777
|
+
self._cache_slots()
|
|
778
|
+
assert self._slots
|
|
779
|
+
return self._slots
|
|
780
|
+
|
|
781
|
+
def _get_free_slot(self) -> int:
|
|
782
|
+
"""
|
|
783
|
+
|
|
784
|
+
:return:
|
|
785
|
+
"""
|
|
786
|
+
if self._slots_by_index is None:
|
|
787
|
+
self._cache_slots()
|
|
788
|
+
assert self._slots_by_index is not None
|
|
789
|
+
slot_indexes = sorted(self._slots_by_index.values())
|
|
790
|
+
if len(slot_indexes) > 0:
|
|
791
|
+
return slot_indexes[-1] + 1
|
|
792
|
+
else:
|
|
793
|
+
return 1
|
|
794
|
+
|
|
795
|
+
def _set_slot(self, slot_index: int, slot: Optional[Slot]):
|
|
796
|
+
"""
|
|
797
|
+
|
|
798
|
+
:param slot_index:
|
|
799
|
+
:param slot:
|
|
800
|
+
:return:
|
|
801
|
+
"""
|
|
802
|
+
assert type(slot_index) == int
|
|
803
|
+
assert 0 < slot_index < 65536
|
|
804
|
+
assert slot is None or isinstance(slot, Slot)
|
|
805
|
+
|
|
806
|
+
if self._slots is None:
|
|
807
|
+
self._cache_slots()
|
|
808
|
+
assert self._slots is not None
|
|
809
|
+
assert self._slots_by_index is not None
|
|
810
|
+
|
|
811
|
+
key = b"\0\0" + struct.pack(">H", slot_index)
|
|
812
|
+
if slot:
|
|
813
|
+
assert slot_index == slot.slot
|
|
814
|
+
assert slot.oid
|
|
815
|
+
|
|
816
|
+
data = cbor2.dumps(slot.marshal())
|
|
817
|
+
with self.begin(write=True) as txn:
|
|
818
|
+
txn._txn.put(key, data)
|
|
819
|
+
self._slots[slot.oid] = slot
|
|
820
|
+
self._slots_by_index[slot.oid] = slot_index
|
|
821
|
+
|
|
822
|
+
self.log.debug(
|
|
823
|
+
"Wrote metadata for table <{oid}> to slot {slot_index:03d}",
|
|
824
|
+
oid=slot.oid,
|
|
825
|
+
slot_index=slot_index,
|
|
826
|
+
)
|
|
827
|
+
else:
|
|
828
|
+
with self.begin(write=True) as txn:
|
|
829
|
+
result = txn.get(key)
|
|
830
|
+
if result:
|
|
831
|
+
txn._txn.delete(key)
|
|
832
|
+
slot = Slot.parse(cbor2.loads(result))
|
|
833
|
+
if slot.oid in self._slots:
|
|
834
|
+
del self._slots[slot.oid]
|
|
835
|
+
if slot.oid in self._slots_by_index:
|
|
836
|
+
del self._slots_by_index[slot.oid]
|
|
837
|
+
|
|
838
|
+
self.log.debug(
|
|
839
|
+
"Deleted metadata for table <{oid}> from slot {slot_index:03d}",
|
|
840
|
+
oid=slot.oid,
|
|
841
|
+
slot_index=slot_index,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
def attach_table(self, klass: Type[_pmap.PersistentMap]):
|
|
845
|
+
"""
|
|
846
|
+
|
|
847
|
+
:param klass:
|
|
848
|
+
:return:
|
|
849
|
+
"""
|
|
850
|
+
if not inspect.isclass(klass):
|
|
851
|
+
raise TypeError(
|
|
852
|
+
"cannot attach object {} as database table: a subclass of zlmdb.PersistentMap is required".format(
|
|
853
|
+
klass
|
|
854
|
+
)
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
name = qual(klass)
|
|
858
|
+
|
|
859
|
+
if not issubclass(klass, _pmap.PersistentMap):
|
|
860
|
+
raise TypeError(
|
|
861
|
+
"cannot attach object of class {} as a database table: a subclass of zlmdb.PersistentMap is required".format(
|
|
862
|
+
name
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
if not hasattr(klass, "_zlmdb_oid") or not klass._zlmdb_oid:
|
|
867
|
+
raise TypeError("{} is not decorated as table slot".format(klass))
|
|
868
|
+
|
|
869
|
+
description = klass.__doc__.strip() if klass.__doc__ else None
|
|
870
|
+
|
|
871
|
+
if self._slots is None:
|
|
872
|
+
self._cache_slots()
|
|
873
|
+
|
|
874
|
+
pmap = self._attach_slot(
|
|
875
|
+
klass._zlmdb_oid,
|
|
876
|
+
klass,
|
|
877
|
+
marshal=klass._zlmdb_marshal,
|
|
878
|
+
parse=klass._zlmdb_parse,
|
|
879
|
+
build=klass._zlmdb_build,
|
|
880
|
+
cast=klass._zlmdb_cast,
|
|
881
|
+
compress=klass._zlmdb_compress,
|
|
882
|
+
create=True,
|
|
883
|
+
name=name,
|
|
884
|
+
description=description,
|
|
885
|
+
)
|
|
886
|
+
return pmap
|
|
887
|
+
|
|
888
|
+
def _attach_slot(
|
|
889
|
+
self,
|
|
890
|
+
oid: uuid.UUID,
|
|
891
|
+
klass: Type[_pmap.PersistentMap],
|
|
892
|
+
marshal: Optional[Callable] = None,
|
|
893
|
+
parse: Optional[Callable] = None,
|
|
894
|
+
build: Optional[Callable] = None,
|
|
895
|
+
cast: Optional[Callable] = None,
|
|
896
|
+
compress: Optional[int] = None,
|
|
897
|
+
create: bool = True,
|
|
898
|
+
name: Optional[str] = None,
|
|
899
|
+
description: Optional[str] = None,
|
|
900
|
+
):
|
|
901
|
+
"""
|
|
902
|
+
|
|
903
|
+
:param oid:
|
|
904
|
+
:param klass:
|
|
905
|
+
:param marshal:
|
|
906
|
+
:param parse:
|
|
907
|
+
:param build:
|
|
908
|
+
:param cast:
|
|
909
|
+
:param compress:
|
|
910
|
+
:param create:
|
|
911
|
+
:param name:
|
|
912
|
+
:param description:
|
|
913
|
+
:return:
|
|
914
|
+
"""
|
|
915
|
+
assert isinstance(oid, uuid.UUID)
|
|
916
|
+
assert issubclass(klass, _pmap.PersistentMap)
|
|
917
|
+
|
|
918
|
+
assert marshal is None or callable(marshal)
|
|
919
|
+
assert parse is None or callable(parse)
|
|
920
|
+
|
|
921
|
+
assert build is None or callable(build)
|
|
922
|
+
assert cast is None or callable(cast)
|
|
923
|
+
|
|
924
|
+
# either marshal+parse (for CBOR/JSON) OR build+cast (for Flatbuffers) OR all unset
|
|
925
|
+
assert (
|
|
926
|
+
(not marshal and not parse and not build and not cast)
|
|
927
|
+
or (not marshal and not parse and build and cast)
|
|
928
|
+
or (marshal and parse and not build and not cast)
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
assert compress is None or compress in [
|
|
932
|
+
_pmap.PersistentMap.COMPRESS_ZLIB,
|
|
933
|
+
_pmap.PersistentMap.COMPRESS_SNAPPY,
|
|
934
|
+
]
|
|
935
|
+
assert type(create) == bool
|
|
936
|
+
assert name is None or type(name) == str
|
|
937
|
+
assert description is None or type(description) == str
|
|
938
|
+
|
|
939
|
+
assert self._slots_by_index is not None
|
|
940
|
+
|
|
941
|
+
if oid not in self._slots_by_index:
|
|
942
|
+
self.log.debug(
|
|
943
|
+
"No slot found in database for DB table <{oid}>: <{name}>",
|
|
944
|
+
name=name,
|
|
945
|
+
oid=oid,
|
|
946
|
+
)
|
|
947
|
+
if create:
|
|
948
|
+
slot_index = self._get_free_slot()
|
|
949
|
+
slot = Slot(
|
|
950
|
+
oid=oid,
|
|
951
|
+
creator="unknown",
|
|
952
|
+
slot=slot_index,
|
|
953
|
+
name=name,
|
|
954
|
+
description=description,
|
|
955
|
+
)
|
|
956
|
+
self._set_slot(slot_index, slot)
|
|
957
|
+
self.log.info(
|
|
958
|
+
"Allocated new slot {slot_index:03d} for database table <{oid}>: {name}",
|
|
959
|
+
slot_index=slot_index,
|
|
960
|
+
oid=oid,
|
|
961
|
+
name=name,
|
|
962
|
+
)
|
|
963
|
+
else:
|
|
964
|
+
raise RuntimeError(
|
|
965
|
+
'No slot found in database for DB table <{}>: "{}"'.format(
|
|
966
|
+
oid, name
|
|
967
|
+
)
|
|
968
|
+
)
|
|
969
|
+
else:
|
|
970
|
+
slot_index = self._slots_by_index[oid]
|
|
971
|
+
# pmap = _pmap.PersistentMap(slot_index)
|
|
972
|
+
# with self.begin() as txn:
|
|
973
|
+
# records = pmap.count(txn)
|
|
974
|
+
self.log.debug(
|
|
975
|
+
"Database table <{name}> attached [oid=<{oid}>, slot=<{slot_index:03d}>]",
|
|
976
|
+
name=name,
|
|
977
|
+
oid=oid,
|
|
978
|
+
slot_index=slot_index,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
if marshal:
|
|
982
|
+
slot_pmap = klass(
|
|
983
|
+
slot_index, marshal=marshal, unmarshal=parse, compress=compress
|
|
984
|
+
) # type: ignore
|
|
985
|
+
elif build:
|
|
986
|
+
slot_pmap = klass(slot_index, build=build, cast=cast, compress=compress) # type: ignore
|
|
987
|
+
else:
|
|
988
|
+
slot_pmap = klass(slot_index, compress=compress)
|
|
989
|
+
|
|
990
|
+
return slot_pmap
|