blob: 6ad93d66f7a1fa63e7065d5664e4618e87ab349c [file] [log] [blame]
tierno87858ca2018-10-08 16:30:15 +02001# -*- coding: utf-8 -*-
2
3# Copyright 2018 Telefonica S.A.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14# implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
tierno5c012612018-04-19 16:01:59 +020018import logging
tierno3054f782018-04-25 16:59:53 +020019from osm_common.dbbase import DbException, DbBase
tierno6472e2b2019-09-02 16:04:16 +000020from osm_common.dbmongo import deep_update
tierno5c012612018-04-19 16:01:59 +020021from http import HTTPStatus
22from uuid import uuid4
23from copy import deepcopy
24
25__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
26
27
28class DbMemory(DbBase):
29
tierno1e9a3292018-11-05 18:18:45 +010030 def __init__(self, logger_name='db', lock=False):
31 super().__init__(logger_name, lock)
tierno5c012612018-04-19 16:01:59 +020032 self.db = {}
33
34 def db_connect(self, config):
tierno87858ca2018-10-08 16:30:15 +020035 """
36 Connect to database
37 :param config: Configuration of database
38 :return: None or raises DbException on error
39 """
tierno5c012612018-04-19 16:01:59 +020040 if "logger_name" in config:
41 self.logger = logging.getLogger(config["logger_name"])
tiernoeef7cb72018-11-12 11:51:49 +010042 master_key = config.get("commonkey") or config.get("masterpassword")
43 if master_key:
44 self.set_secret_key(master_key)
tierno5c012612018-04-19 16:01:59 +020045
46 @staticmethod
tierno87858ca2018-10-08 16:30:15 +020047 def _format_filter(q_filter):
tierno6472e2b2019-09-02 16:04:16 +000048 db_filter = {}
49 # split keys with ANYINDEX in this way:
50 # {"A.B.ANYINDEX.C.D.ANYINDEX.E": v } -> {"A.B.ANYINDEX": {"C.D.ANYINDEX": {"E": v}}}
51 if q_filter:
52 for k, v in q_filter.items():
53 db_v = v
54 kleft, _, kright = k.rpartition(".ANYINDEX.")
55 while kleft:
56 k = kleft + ".ANYINDEX"
57 db_v = {kright: db_v}
58 kleft, _, kright = k.rpartition(".ANYINDEX.")
59 deep_update(db_filter, {k: db_v})
60
61 return db_filter
tierno5c012612018-04-19 16:01:59 +020062
tierno87858ca2018-10-08 16:30:15 +020063 def _find(self, table, q_filter):
tierno6472e2b2019-09-02 16:04:16 +000064
tierno40e326a2019-09-19 09:23:44 +000065 def recursive_find(key_list, key_next_index, content, oper, target):
tierno6472e2b2019-09-02 16:04:16 +000066 if key_next_index == len(key_list) or content is None:
67 try:
tierno40e326a2019-09-19 09:23:44 +000068 if oper in ("eq", "cont"):
69 if isinstance(target, list):
70 if isinstance(content, list):
71 return any(content_item in target for content_item in content)
72 return content in target
73 elif isinstance(content, list):
74 return target in content
75 else:
76 return content == target
77 elif oper in ("neq", "ne", "ncont"):
78 if isinstance(target, list):
79 if isinstance(content, list):
80 return all(content_item not in target for content_item in content)
81 return content not in target
82 elif isinstance(content, list):
83 return target not in content
84 else:
85 return content != target
86 if oper == "gt":
tierno6472e2b2019-09-02 16:04:16 +000087 return content > target
tierno40e326a2019-09-19 09:23:44 +000088 elif oper == "gte":
tierno6472e2b2019-09-02 16:04:16 +000089 return content >= target
tierno40e326a2019-09-19 09:23:44 +000090 elif oper == "lt":
tierno6472e2b2019-09-02 16:04:16 +000091 return content < target
tierno40e326a2019-09-19 09:23:44 +000092 elif oper == "lte":
tierno6472e2b2019-09-02 16:04:16 +000093 return content <= target
tierno6472e2b2019-09-02 16:04:16 +000094 else:
95 raise DbException("Unknown filter operator '{}' in key '{}'".
tierno40e326a2019-09-19 09:23:44 +000096 format(oper, ".".join(key_list)), http_code=HTTPStatus.BAD_REQUEST)
tierno6472e2b2019-09-02 16:04:16 +000097 except TypeError:
98 return False
99
100 elif isinstance(content, dict):
tierno40e326a2019-09-19 09:23:44 +0000101 return recursive_find(key_list, key_next_index + 1, content.get(key_list[key_next_index]), oper,
tierno6472e2b2019-09-02 16:04:16 +0000102 target)
103 elif isinstance(content, list):
104 look_for_match = True # when there is a match return immediately
tierno40e326a2019-09-19 09:23:44 +0000105 if (target is None) != (oper in ("neq", "ne", "ncont")): # one True and other False (Xor)
106 look_for_match = False # when there is not a match return immediately
tierno6472e2b2019-09-02 16:04:16 +0000107
108 for content_item in content:
109 if key_list[key_next_index] == "ANYINDEX" and isinstance(v, dict):
tierno40e326a2019-09-19 09:23:44 +0000110 matches = True
tierno6472e2b2019-09-02 16:04:16 +0000111 for k2, v2 in target.items():
112 k_new_list = k2.split(".")
113 new_operator = "eq"
114 if k_new_list[-1] in ("eq", "ne", "gt", "gte", "lt", "lte", "cont", "ncont", "neq"):
115 new_operator = k_new_list.pop()
116 if not recursive_find(k_new_list, 0, content_item, new_operator, v2):
tierno40e326a2019-09-19 09:23:44 +0000117 matches = False
tierno6472e2b2019-09-02 16:04:16 +0000118 break
tierno6472e2b2019-09-02 16:04:16 +0000119
120 else:
tierno40e326a2019-09-19 09:23:44 +0000121 matches = recursive_find(key_list, key_next_index, content_item, oper, target)
122 if matches == look_for_match:
123 return matches
tierno6472e2b2019-09-02 16:04:16 +0000124 if key_list[key_next_index].isdecimal() and int(key_list[key_next_index]) < len(content):
tierno40e326a2019-09-19 09:23:44 +0000125 matches = recursive_find(key_list, key_next_index + 1, content[int(key_list[key_next_index])],
126 oper, target)
127 if matches == look_for_match:
128 return matches
tierno6472e2b2019-09-02 16:04:16 +0000129 return not look_for_match
130 else: # content is not dict, nor list neither None, so not found
tierno40e326a2019-09-19 09:23:44 +0000131 if oper in ("neq", "ne", "ncont"):
132 return target is not None
tierno6472e2b2019-09-02 16:04:16 +0000133 else:
tierno40e326a2019-09-19 09:23:44 +0000134 return target is None
tierno6472e2b2019-09-02 16:04:16 +0000135
tierno5c012612018-04-19 16:01:59 +0200136 for i, row in enumerate(self.db.get(table, ())):
tierno6472e2b2019-09-02 16:04:16 +0000137 q_filter = q_filter or {}
138 for k, v in q_filter.items():
139 k_list = k.split(".")
140 operator = "eq"
141 if k_list[-1] in ("eq", "ne", "gt", "gte", "lt", "lte", "cont", "ncont", "neq"):
142 operator = k_list.pop()
tierno40e326a2019-09-19 09:23:44 +0000143 matches = recursive_find(k_list, 0, row, operator, v)
144 if not matches:
tierno6472e2b2019-09-02 16:04:16 +0000145 break
146 else:
147 # match
tierno5c012612018-04-19 16:01:59 +0200148 yield i, row
149
tierno87858ca2018-10-08 16:30:15 +0200150 def get_list(self, table, q_filter=None):
151 """
152 Obtain a list of entries matching q_filter
153 :param table: collection or table
154 :param q_filter: Filter
155 :return: a list (can be empty) with the found entries. Raises DbException on error
156 """
tierno5c012612018-04-19 16:01:59 +0200157 try:
tiernob20a9022018-05-22 12:07:05 +0200158 result = []
tierno1e9a3292018-11-05 18:18:45 +0100159 with self.lock:
160 for _, row in self._find(table, self._format_filter(q_filter)):
161 result.append(deepcopy(row))
tiernob20a9022018-05-22 12:07:05 +0200162 return result
tierno5c012612018-04-19 16:01:59 +0200163 except DbException:
164 raise
165 except Exception as e: # TODO refine
166 raise DbException(str(e))
167
delacruzramoae049d82019-09-17 16:05:17 +0200168 def count(self, table, q_filter=None):
169 """
170 Count the number of entries matching q_filter
171 :param table: collection or table
172 :param q_filter: Filter
173 :return: number of entries found (can be zero)
174 :raise: DbException on error
175 """
176 try:
177 with self.lock:
178 return sum(1 for x in self._find(table, self._format_filter(q_filter)))
179 except DbException:
180 raise
181 except Exception as e: # TODO refine
182 raise DbException(str(e))
183
tierno87858ca2018-10-08 16:30:15 +0200184 def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True):
185 """
186 Obtain one entry matching q_filter
187 :param table: collection or table
188 :param q_filter: Filter
189 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
190 it raises a DbException
191 :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so
192 that it raises a DbException
193 :return: The requested element, or None
194 """
tierno5c012612018-04-19 16:01:59 +0200195 try:
tiernob20a9022018-05-22 12:07:05 +0200196 result = None
tierno1e9a3292018-11-05 18:18:45 +0100197 with self.lock:
198 for _, row in self._find(table, self._format_filter(q_filter)):
199 if not fail_on_more:
200 return deepcopy(row)
201 if result:
202 raise DbException("Found more than one entry with filter='{}'".format(q_filter),
203 HTTPStatus.CONFLICT.value)
204 result = row
tiernob20a9022018-05-22 12:07:05 +0200205 if not result and fail_on_empty:
tierno87858ca2018-10-08 16:30:15 +0200206 raise DbException("Not found entry with filter='{}'".format(q_filter), HTTPStatus.NOT_FOUND)
tiernob20a9022018-05-22 12:07:05 +0200207 return deepcopy(result)
tierno5c012612018-04-19 16:01:59 +0200208 except Exception as e: # TODO refine
209 raise DbException(str(e))
210
tierno87858ca2018-10-08 16:30:15 +0200211 def del_list(self, table, q_filter=None):
212 """
213 Deletes all entries that match q_filter
214 :param table: collection or table
215 :param q_filter: Filter
216 :return: Dict with the number of entries deleted
217 """
tierno5c012612018-04-19 16:01:59 +0200218 try:
219 id_list = []
tierno1e9a3292018-11-05 18:18:45 +0100220 with self.lock:
221 for i, _ in self._find(table, self._format_filter(q_filter)):
222 id_list.append(i)
tierno5c012612018-04-19 16:01:59 +0200223 deleted = len(id_list)
Eduardo Sousa857731b2018-04-26 15:55:05 +0100224 for i in reversed(id_list):
tierno5c012612018-04-19 16:01:59 +0200225 del self.db[table][i]
226 return {"deleted": deleted}
227 except DbException:
228 raise
229 except Exception as e: # TODO refine
230 raise DbException(str(e))
231
tierno87858ca2018-10-08 16:30:15 +0200232 def del_one(self, table, q_filter=None, fail_on_empty=True):
233 """
234 Deletes one entry that matches q_filter
235 :param table: collection or table
236 :param q_filter: Filter
237 :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in
238 which case it raises a DbException
239 :return: Dict with the number of entries deleted
240 """
tierno5c012612018-04-19 16:01:59 +0200241 try:
tierno1e9a3292018-11-05 18:18:45 +0100242 with self.lock:
243 for i, _ in self._find(table, self._format_filter(q_filter)):
244 break
245 else:
246 if fail_on_empty:
247 raise DbException("Not found entry with filter='{}'".format(q_filter), HTTPStatus.NOT_FOUND)
248 return None
249 del self.db[table][i]
tierno5c012612018-04-19 16:01:59 +0200250 return {"deleted": 1}
251 except Exception as e: # TODO refine
252 raise DbException(str(e))
253
tierno399f6c32020-05-12 07:36:41 +0000254 def _update(self, db_item, update_dict, unset=None, pull=None, push=None, push_list=None):
tierno7fc50dd2020-02-17 12:01:38 +0000255 """
256 Modifies an entry at database
257 :param db_item: entry of the table to update
258 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
259 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
260 ignored. If not exist, it is ignored
261 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
262 if exist in the array is removed. If not exist, it is ignored
263 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
264 is appended to the end of the array
tierno399f6c32020-05-12 07:36:41 +0000265 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
266 whole array
tierno7fc50dd2020-02-17 12:01:38 +0000267 :return: True if database has been changed, False if not; Exception on error
268 """
269 def _iterate_keys(k, db_nested, populate=True):
270 k_list = k.split(".")
271 k_item_prev = k_list[0]
272 populated = False
tiernobf6c5722020-03-12 09:54:35 +0000273 if k_item_prev not in db_nested and populate:
274 populated = True
275 db_nested[k_item_prev] = None
tierno7fc50dd2020-02-17 12:01:38 +0000276 for k_item in k_list[1:]:
277 if isinstance(db_nested[k_item_prev], dict):
278 if k_item not in db_nested[k_item_prev]:
279 if not populate:
280 raise DbException("Cannot set '{}', not existing '{}'".format(k, k_item))
281 populated = True
282 db_nested[k_item_prev][k_item] = None
283 elif isinstance(db_nested[k_item_prev], list) and k_item.isdigit():
284 # extend list with Nones if index greater than list
285 k_item = int(k_item)
286 if k_item >= len(db_nested[k_item_prev]):
287 if not populate:
288 raise DbException("Cannot set '{}', index too large '{}'".format(k, k_item))
289 populated = True
290 db_nested[k_item_prev] += [None] * (k_item - len(db_nested[k_item_prev]) + 1)
291 elif db_nested[k_item_prev] is None:
292 if not populate:
293 raise DbException("Cannot set '{}', not existing '{}'".format(k, k_item))
294 populated = True
295 db_nested[k_item_prev] = {k_item: None}
296 else: # number, string, boolean, ... or list but with not integer key
297 raise DbException("Cannot set '{}' on existing '{}={}'".format(k, k_item_prev,
298 db_nested[k_item_prev]))
299 db_nested = db_nested[k_item_prev]
300 k_item_prev = k_item
301 return db_nested, k_item_prev, populated
302
303 updated = False
304 try:
305 if update_dict:
306 for dot_k, v in update_dict.items():
307 dict_to_update, key_to_update, _ = _iterate_keys(dot_k, db_item)
308 dict_to_update[key_to_update] = v
309 updated = True
310 if unset:
311 for dot_k in unset:
312 try:
313 dict_to_update, key_to_update, _ = _iterate_keys(dot_k, db_item, populate=False)
314 del dict_to_update[key_to_update]
315 updated = True
316 except Exception:
317 pass
318 if pull:
319 for dot_k, v in pull.items():
320 try:
321 dict_to_update, key_to_update, _ = _iterate_keys(dot_k, db_item, populate=False)
322 except Exception:
323 continue
324 if key_to_update not in dict_to_update:
325 continue
326 if not isinstance(dict_to_update[key_to_update], list):
327 raise DbException("Cannot pull '{}'. Target is not a list".format(dot_k))
328 while v in dict_to_update[key_to_update]:
329 dict_to_update[key_to_update].remove(v)
330 updated = True
331 if push:
332 for dot_k, v in push.items():
333 dict_to_update, key_to_update, populated = _iterate_keys(dot_k, db_item)
334 if isinstance(dict_to_update, dict) and key_to_update not in dict_to_update:
335 dict_to_update[key_to_update] = [v]
336 updated = True
337 elif populated and dict_to_update[key_to_update] is None:
338 dict_to_update[key_to_update] = [v]
339 updated = True
340 elif not isinstance(dict_to_update[key_to_update], list):
341 raise DbException("Cannot push '{}'. Target is not a list".format(dot_k))
342 else:
343 dict_to_update[key_to_update].append(v)
344 updated = True
tierno399f6c32020-05-12 07:36:41 +0000345 if push_list:
346 for dot_k, v in push_list.items():
347 if not isinstance(v, list):
348 raise DbException("Invalid content at push_list, '{}' must be an array".format(dot_k),
349 http_code=HTTPStatus.BAD_REQUEST)
350 dict_to_update, key_to_update, populated = _iterate_keys(dot_k, db_item)
351 if isinstance(dict_to_update, dict) and key_to_update not in dict_to_update:
352 dict_to_update[key_to_update] = v.copy()
353 updated = True
354 elif populated and dict_to_update[key_to_update] is None:
355 dict_to_update[key_to_update] = v.copy()
356 updated = True
357 elif not isinstance(dict_to_update[key_to_update], list):
358 raise DbException("Cannot push '{}'. Target is not a list".format(dot_k),
359 http_code=HTTPStatus.CONFLICT)
360 else:
361 dict_to_update[key_to_update] += v
362 updated = True
tierno7fc50dd2020-02-17 12:01:38 +0000363
364 return updated
365 except DbException:
366 raise
367 except Exception as e: # TODO refine
368 raise DbException(str(e))
369
tierno399f6c32020-05-12 07:36:41 +0000370 def set_one(self, table, q_filter, update_dict, fail_on_empty=True, unset=None, pull=None, push=None,
371 push_list=None):
tierno6472e2b2019-09-02 16:04:16 +0000372 """
373 Modifies an entry at database
374 :param table: collection or table
375 :param q_filter: Filter
376 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
377 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
378 it raises a DbException
379 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
380 ignored. If not exist, it is ignored
381 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
382 if exist in the array is removed. If not exist, it is ignored
383 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
384 is appended to the end of the array
tierno399f6c32020-05-12 07:36:41 +0000385 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
386 whole array
tierno6472e2b2019-09-02 16:04:16 +0000387 :return: Dict with the number of entries modified. None if no matching is found.
388 """
tierno7fc50dd2020-02-17 12:01:38 +0000389 with self.lock:
390 for i, db_item in self._find(table, self._format_filter(q_filter)):
tierno399f6c32020-05-12 07:36:41 +0000391 updated = self._update(db_item, update_dict, unset=unset, pull=pull, push=push, push_list=push_list)
tierno7fc50dd2020-02-17 12:01:38 +0000392 return {"updated": 1 if updated else 0}
393 else:
394 if fail_on_empty:
395 raise DbException("Not found entry with _id='{}'".format(q_filter), HTTPStatus.NOT_FOUND)
396 return None
tierno6472e2b2019-09-02 16:04:16 +0000397
tierno399f6c32020-05-12 07:36:41 +0000398 def set_list(self, table, q_filter, update_dict, unset=None, pull=None, push=None, push_list=None):
399 """Modifies al matching entries at database. Same as push. Do not fail if nothing matches"""
tierno7fc50dd2020-02-17 12:01:38 +0000400 with self.lock:
401 updated = 0
tierno77e2d6a2020-03-18 07:31:54 +0000402 found = 0
403 for _, db_item in self._find(table, self._format_filter(q_filter)):
404 found += 1
tierno399f6c32020-05-12 07:36:41 +0000405 if self._update(db_item, update_dict, unset=unset, pull=pull, push=push, push_list=push_list):
tierno7fc50dd2020-02-17 12:01:38 +0000406 updated += 1
tierno70911f02020-03-30 08:56:15 +0000407 # if not found and fail_on_empty:
408 # raise DbException("Not found entry with '{}'".format(q_filter), HTTPStatus.NOT_FOUND)
tierno77e2d6a2020-03-18 07:31:54 +0000409 return {"updated": updated} if found else None
tierno6472e2b2019-09-02 16:04:16 +0000410
tierno87858ca2018-10-08 16:30:15 +0200411 def replace(self, table, _id, indata, fail_on_empty=True):
412 """
413 Replace the content of an entry
414 :param table: collection or table
415 :param _id: internal database id
416 :param indata: content to replace
417 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
418 it raises a DbException
419 :return: Dict with the number of entries replaced
420 """
tierno5c012612018-04-19 16:01:59 +0200421 try:
tierno1e9a3292018-11-05 18:18:45 +0100422 with self.lock:
423 for i, _ in self._find(table, self._format_filter({"_id": _id})):
424 break
425 else:
426 if fail_on_empty:
427 raise DbException("Not found entry with _id='{}'".format(_id), HTTPStatus.NOT_FOUND)
428 return None
429 self.db[table][i] = deepcopy(indata)
Eduardo Sousa22f0fcd2018-04-26 15:43:28 +0100430 return {"updated": 1}
tierno136f2952018-10-19 13:01:03 +0200431 except DbException:
432 raise
tierno5c012612018-04-19 16:01:59 +0200433 except Exception as e: # TODO refine
434 raise DbException(str(e))
435
436 def create(self, table, indata):
tierno87858ca2018-10-08 16:30:15 +0200437 """
438 Add a new entry at database
439 :param table: collection or table
440 :param indata: content to be added
tierno2c9794c2020-04-29 10:24:28 +0000441 :return: database '_id' of the inserted element. Raises a DbException on error
tierno87858ca2018-10-08 16:30:15 +0200442 """
tierno5c012612018-04-19 16:01:59 +0200443 try:
444 id = indata.get("_id")
445 if not id:
446 id = str(uuid4())
447 indata["_id"] = id
tierno1e9a3292018-11-05 18:18:45 +0100448 with self.lock:
449 if table not in self.db:
450 self.db[table] = []
451 self.db[table].append(deepcopy(indata))
tierno5c012612018-04-19 16:01:59 +0200452 return id
453 except Exception as e: # TODO refine
454 raise DbException(str(e))
455
tierno6472e2b2019-09-02 16:04:16 +0000456 def create_list(self, table, indata_list):
457 """
458 Add a new entry at database
459 :param table: collection or table
460 :param indata_list: list content to be added
tierno2c9794c2020-04-29 10:24:28 +0000461 :return: list of inserted 'id's. Raises a DbException on error
tierno6472e2b2019-09-02 16:04:16 +0000462 """
463 try:
464 _ids = []
tierno40e326a2019-09-19 09:23:44 +0000465 with self.lock:
466 for indata in indata_list:
467 _id = indata.get("_id")
468 if not _id:
469 _id = str(uuid4())
470 indata["_id"] = _id
471 with self.lock:
472 if table not in self.db:
473 self.db[table] = []
474 self.db[table].append(deepcopy(indata))
475 _ids.append(_id)
tierno6472e2b2019-09-02 16:04:16 +0000476 return _ids
477 except Exception as e: # TODO refine
478 raise DbException(str(e))
479
tierno5c012612018-04-19 16:01:59 +0200480
481if __name__ == '__main__':
482 # some test code
tierno3054f782018-04-25 16:59:53 +0200483 db = DbMemory()
tierno5c012612018-04-19 16:01:59 +0200484 db.create("test", {"_id": 1, "data": 1})
485 db.create("test", {"_id": 2, "data": 2})
486 db.create("test", {"_id": 3, "data": 3})
487 print("must be 3 items:", db.get_list("test"))
488 print("must return item 2:", db.get_list("test", {"_id": 2}))
489 db.del_one("test", {"_id": 2})
490 print("must be emtpy:", db.get_list("test", {"_id": 2}))