soupdb 0.6.1__py3-none-any.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.
Files changed (96) hide show
  1. engine/__init__.py +0 -0
  2. engine/__main__.py +86 -0
  3. engine/algorithms/__init__.py +0 -0
  4. engine/algorithms/external_hash.py +435 -0
  5. engine/algorithms/external_sort.py +307 -0
  6. engine/algorithms/spatial/__init__.py +44 -0
  7. engine/algorithms/spatial/bounds.py +40 -0
  8. engine/algorithms/spatial/geometry.py +109 -0
  9. engine/algorithms/spatial/knn.py +219 -0
  10. engine/algorithms/spatial/metrics.py +96 -0
  11. engine/algorithms/spatial/polygon_query.py +49 -0
  12. engine/algorithms/spatial/predicates.py +57 -0
  13. engine/algorithms/spatial/range_query.py +121 -0
  14. engine/algorithms/spatial/stats.py +51 -0
  15. engine/algorithms/spatial/traversal.py +42 -0
  16. engine/algorithms/spatial/validation.py +89 -0
  17. engine/common/__init__.py +0 -0
  18. engine/common/catalog.py +885 -0
  19. engine/common/errors.py +47 -0
  20. engine/common/page.py +1 -0
  21. engine/common/ranked.py +87 -0
  22. engine/common/ranked_catalog.py +88 -0
  23. engine/common/record.py +181 -0
  24. engine/common/rid.py +17 -0
  25. engine/common/schema.py +28 -0
  26. engine/indexes/__init__.py +0 -0
  27. engine/indexes/_bplus_page.py +312 -0
  28. engine/indexes/_hash_page.py +217 -0
  29. engine/indexes/_key_codec.py +111 -0
  30. engine/indexes/base.py +78 -0
  31. engine/indexes/bplus_tree.py +579 -0
  32. engine/indexes/clustered_bplus.py +98 -0
  33. engine/indexes/extendible_hash.py +296 -0
  34. engine/indexes/inverted/__init__.py +41 -0
  35. engine/indexes/ranked_factory.py +130 -0
  36. engine/indexes/rtree/__init__.py +9 -0
  37. engine/indexes/rtree/_codec.py +97 -0
  38. engine/indexes/rtree/_page_codec.py +140 -0
  39. engine/indexes/rtree/_page_store.py +208 -0
  40. engine/indexes/rtree/mbr.py +55 -0
  41. engine/indexes/rtree/node.py +66 -0
  42. engine/indexes/rtree/point.py +33 -0
  43. engine/indexes/rtree/queries.py +170 -0
  44. engine/indexes/rtree/rtree.py +268 -0
  45. engine/indexes/vector/__init__.py +51 -0
  46. engine/multimedia/__init__.py +35 -0
  47. engine/plugins/__init__.py +26 -0
  48. engine/plugins/base.py +85 -0
  49. engine/plugins/capabilities.py +27 -0
  50. engine/plugins/errors.py +15 -0
  51. engine/plugins/registry.py +258 -0
  52. engine/query/__init__.py +20 -0
  53. engine/query/ast.py +406 -0
  54. engine/query/errors.py +10 -0
  55. engine/query/evaluator.py +300 -0
  56. engine/query/executor.py +307 -0
  57. engine/query/format_expr.py +97 -0
  58. engine/query/index_maintenance.py +84 -0
  59. engine/query/lexer.py +215 -0
  60. engine/query/operators.py +1054 -0
  61. engine/query/params.py +34 -0
  62. engine/query/parser.py +1013 -0
  63. engine/query/planner.py +906 -0
  64. engine/query/ranked_binding.py +105 -0
  65. engine/query/ranked_candidates.py +47 -0
  66. engine/query/ranked_operators.py +209 -0
  67. engine/query/ranked_planning.py +193 -0
  68. engine/query/ranked_vector_planning.py +110 -0
  69. engine/query/resultset.py +20 -0
  70. engine/query/spatial_metrics.py +181 -0
  71. engine/query/tokens.py +36 -0
  72. engine/query/vector_operators.py +106 -0
  73. engine/storage/__init__.py +0 -0
  74. engine/storage/_sequential_page.py +63 -0
  75. engine/storage/_slotted_page.py +141 -0
  76. engine/storage/base.py +45 -0
  77. engine/storage/buffer_manager.py +77 -0
  78. engine/storage/disk_manager.py +93 -0
  79. engine/storage/file_manager.py +61 -0
  80. engine/storage/heap_file.py +150 -0
  81. engine/storage/io_counters.py +33 -0
  82. engine/storage/sequential_file.py +351 -0
  83. engine/transactions/__init__.py +0 -0
  84. engine/transactions/base.py +67 -0
  85. engine/transactions/connection_handler.py +208 -0
  86. engine/transactions/demo.py +130 -0
  87. engine/transactions/lock_manager.py +44 -0
  88. engine/transactions/protocol.py +457 -0
  89. engine/transactions/session.py +403 -0
  90. engine/transactions/strategies/__init__.py +0 -0
  91. engine/transactions/strategies/strict_2pl.py +165 -0
  92. engine/transactions/transaction_manager.py +75 -0
  93. soupdb-0.6.1.dist-info/METADATA +132 -0
  94. soupdb-0.6.1.dist-info/RECORD +96 -0
  95. soupdb-0.6.1.dist-info/WHEEL +4 -0
  96. soupdb-0.6.1.dist-info/licenses/LICENSE +21 -0
engine/__init__.py ADDED
File without changes
engine/__main__.py ADDED
@@ -0,0 +1,86 @@
1
+ """Punto de entrada del gestor: ``python -m engine``.
2
+
3
+ Levanta el servidor TCP que habla el protocolo binario con el driver y con
4
+ SoupChef. Sin esto el motor solo se puede usar escribiendo Python a mano, y el
5
+ contenedor no sirve nada en su puerto.
6
+
7
+ Toda la configuración llega por variables de entorno, que es lo que espera
8
+ ``docker-compose.yml``:
9
+
10
+ ==================== ========================== ======================
11
+ Variable Valor por defecto Qué controla
12
+ ==================== ========================== ======================
13
+ ``SOUPDB_DATA`` ``data`` Directorio de la base
14
+ ``DRIVER_HOST`` ``0.0.0.0`` Interfaz de escucha
15
+ ``DRIVER_PORT`` ``55432`` Puerto
16
+ ``PAGE_SIZE`` ``4096`` Tamaño de página
17
+ ``BUFFER_POOL_SIZE`` ``1024`` Páginas en el buffer pool
18
+ ``MAX_CONNECTIONS`` ``100`` Conexiones simultáneas
19
+ ``LOCK_TIMEOUT_MS`` ``5000`` Espera máxima por un lock
20
+ ==================== ========================== ======================
21
+
22
+ Escucha en ``0.0.0.0`` y no en ``127.0.0.1`` porque dentro de un contenedor una
23
+ escucha en loopback no es alcanzable desde el host.
24
+ """
25
+
26
+ import os
27
+ import signal
28
+ import sys
29
+ from pathlib import Path
30
+ from types import FrameType
31
+
32
+ from engine.common.catalog import Catalog
33
+ from engine.transactions.connection_handler import ConnectionHandler
34
+ from engine.transactions.transaction_manager import TransactionManager
35
+
36
+
37
+ def _env_int(name: str, default: int) -> int:
38
+ raw = os.environ.get(name)
39
+ if raw is None or raw.strip() == "":
40
+ return default
41
+ try:
42
+ return int(raw)
43
+ except ValueError:
44
+ raise SystemExit(f"{name} debe ser un entero, no {raw!r}") from None
45
+
46
+
47
+ def main() -> None:
48
+ data_dir = Path(os.environ.get("SOUPDB_DATA", "data"))
49
+ data_dir.mkdir(parents=True, exist_ok=True)
50
+
51
+ catalog = Catalog(
52
+ data_dir,
53
+ page_size=_env_int("PAGE_SIZE", 4096),
54
+ buffer_capacity=_env_int("BUFFER_POOL_SIZE", 1024),
55
+ )
56
+ handler = ConnectionHandler(
57
+ catalog,
58
+ TransactionManager(),
59
+ host=os.environ.get("DRIVER_HOST", "0.0.0.0"),
60
+ port=_env_int("DRIVER_PORT", 55432),
61
+ max_connections=_env_int("MAX_CONNECTIONS", 100),
62
+ lock_timeout_ms=_env_int("LOCK_TIMEOUT_MS", 5000),
63
+ )
64
+ handler.bind()
65
+
66
+ def _shutdown(_signum: int, _frame: FrameType | None) -> None:
67
+ # Docker manda SIGTERM al parar el contenedor: hay que cerrar los
68
+ # archivos para no dejar la base a medio escribir.
69
+ handler.shutdown()
70
+ catalog.close()
71
+ sys.exit(0)
72
+
73
+ signal.signal(signal.SIGTERM, _shutdown)
74
+ signal.signal(signal.SIGINT, _shutdown)
75
+
76
+ host = os.environ.get("DRIVER_HOST", "0.0.0.0")
77
+ print(f"SoupDB escuchando en {host}:{handler.port}, datos en {data_dir}", flush=True)
78
+ try:
79
+ handler.serve_forever()
80
+ finally:
81
+ handler.shutdown()
82
+ catalog.close()
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main()
File without changes
@@ -0,0 +1,435 @@
1
+ """External hashing for GROUP BY and hash JOIN."""
2
+
3
+ import hashlib
4
+ import struct
5
+ import tempfile
6
+ from collections.abc import Callable, Iterable, Iterator
7
+ from pathlib import Path
8
+ from typing import BinaryIO, TypeVar
9
+
10
+ from engine.common.record import Record
11
+ from engine.indexes._key_codec import encode_key
12
+ from engine.indexes.base import Key
13
+
14
+ Aggregate = TypeVar("Aggregate")
15
+
16
+ _LENGTH_FORMAT = "<I"
17
+ _LENGTH_SIZE = struct.calcsize(_LENGTH_FORMAT)
18
+
19
+
20
+ def external_hash_group_by(
21
+ records: Iterable[Record],
22
+ key_fn: Callable[[Record], Key],
23
+ aggregate_step: Callable[[Aggregate | None, Record], Aggregate],
24
+ *,
25
+ memory_limit_bytes: int,
26
+ partition_count: int = 8,
27
+ max_partition_depth: int = 4,
28
+ temp_dir: str | Path | None = None,
29
+ ) -> Iterator[tuple[Key, Aggregate]]:
30
+ """Group records using disk-backed hash partitioning."""
31
+ _validate_parameters(
32
+ memory_limit_bytes,
33
+ partition_count,
34
+ max_partition_depth,
35
+ )
36
+
37
+ with tempfile.TemporaryDirectory(dir=temp_dir) as working_directory:
38
+ workdir = Path(working_directory)
39
+
40
+ partitions = _partition_records(
41
+ records,
42
+ key_fn,
43
+ workdir=workdir,
44
+ prefix="group",
45
+ partition_count=partition_count,
46
+ level=0,
47
+ )
48
+
49
+ for partition in partitions:
50
+ yield from _group_partition(
51
+ partition,
52
+ key_fn,
53
+ aggregate_step,
54
+ workdir=workdir,
55
+ memory_limit_bytes=memory_limit_bytes,
56
+ partition_count=partition_count,
57
+ level=1,
58
+ max_partition_depth=max_partition_depth,
59
+ )
60
+
61
+
62
+ def external_hash_join(
63
+ left_records: Iterable[Record],
64
+ right_records: Iterable[Record],
65
+ left_key_fn: Callable[[Record], Key],
66
+ right_key_fn: Callable[[Record], Key],
67
+ *,
68
+ memory_limit_bytes: int,
69
+ partition_count: int = 8,
70
+ max_partition_depth: int = 4,
71
+ temp_dir: str | Path | None = None,
72
+ ) -> Iterator[tuple[Record, Record]]:
73
+ """Perform an equality join using external hash partitioning."""
74
+ _validate_parameters(
75
+ memory_limit_bytes,
76
+ partition_count,
77
+ max_partition_depth,
78
+ )
79
+
80
+ with tempfile.TemporaryDirectory(dir=temp_dir) as working_directory:
81
+ workdir = Path(working_directory)
82
+
83
+ left_partitions = _partition_records(
84
+ left_records,
85
+ left_key_fn,
86
+ workdir=workdir,
87
+ prefix="join-left",
88
+ partition_count=partition_count,
89
+ level=0,
90
+ )
91
+ right_partitions = _partition_records(
92
+ right_records,
93
+ right_key_fn,
94
+ workdir=workdir,
95
+ prefix="join-right",
96
+ partition_count=partition_count,
97
+ level=0,
98
+ )
99
+
100
+ for left_path, right_path in zip(
101
+ left_partitions,
102
+ right_partitions,
103
+ strict=True,
104
+ ):
105
+ yield from _join_partition(
106
+ left_path,
107
+ right_path,
108
+ left_key_fn,
109
+ right_key_fn,
110
+ workdir=workdir,
111
+ memory_limit_bytes=memory_limit_bytes,
112
+ partition_count=partition_count,
113
+ level=1,
114
+ max_partition_depth=max_partition_depth,
115
+ )
116
+
117
+
118
+ def _group_partition(
119
+ path: Path,
120
+ key_fn: Callable[[Record], Key],
121
+ aggregate_step: Callable[[Aggregate | None, Record], Aggregate],
122
+ *,
123
+ workdir: Path,
124
+ memory_limit_bytes: int,
125
+ partition_count: int,
126
+ level: int,
127
+ max_partition_depth: int,
128
+ ) -> Iterator[tuple[Key, Aggregate]]:
129
+ if path.stat().st_size > memory_limit_bytes and level <= max_partition_depth:
130
+ children = _partition_records(
131
+ _read_records(path),
132
+ key_fn,
133
+ workdir=workdir,
134
+ prefix=f"group-{level}-{path.stem}",
135
+ partition_count=partition_count,
136
+ level=level,
137
+ )
138
+
139
+ for child in children:
140
+ yield from _group_partition(
141
+ child,
142
+ key_fn,
143
+ aggregate_step,
144
+ workdir=workdir,
145
+ memory_limit_bytes=memory_limit_bytes,
146
+ partition_count=partition_count,
147
+ level=level + 1,
148
+ max_partition_depth=max_partition_depth,
149
+ )
150
+ return
151
+
152
+ groups: dict[Key, Aggregate] = {}
153
+
154
+ for record in _read_records(path):
155
+ key = key_fn(record)
156
+ groups[key] = aggregate_step(groups.get(key), record)
157
+
158
+ yield from groups.items()
159
+
160
+
161
+ def _join_partition(
162
+ left_path: Path,
163
+ right_path: Path,
164
+ left_key_fn: Callable[[Record], Key],
165
+ right_key_fn: Callable[[Record], Key],
166
+ *,
167
+ workdir: Path,
168
+ memory_limit_bytes: int,
169
+ partition_count: int,
170
+ level: int,
171
+ max_partition_depth: int,
172
+ ) -> Iterator[tuple[Record, Record]]:
173
+ if left_path.stat().st_size == 0 or right_path.stat().st_size == 0:
174
+ return
175
+
176
+ smaller_size = min(
177
+ left_path.stat().st_size,
178
+ right_path.stat().st_size,
179
+ )
180
+
181
+ if smaller_size <= memory_limit_bytes:
182
+ yield from _in_memory_join(
183
+ left_path,
184
+ right_path,
185
+ left_key_fn,
186
+ right_key_fn,
187
+ )
188
+ return
189
+
190
+ if level <= max_partition_depth:
191
+ left_children = _partition_records(
192
+ _read_records(left_path),
193
+ left_key_fn,
194
+ workdir=workdir,
195
+ prefix=f"left-{level}-{left_path.stem}",
196
+ partition_count=partition_count,
197
+ level=level,
198
+ )
199
+ right_children = _partition_records(
200
+ _read_records(right_path),
201
+ right_key_fn,
202
+ workdir=workdir,
203
+ prefix=f"right-{level}-{right_path.stem}",
204
+ partition_count=partition_count,
205
+ level=level,
206
+ )
207
+
208
+ for child_left, child_right in zip(
209
+ left_children,
210
+ right_children,
211
+ strict=True,
212
+ ):
213
+ yield from _join_partition(
214
+ child_left,
215
+ child_right,
216
+ left_key_fn,
217
+ right_key_fn,
218
+ workdir=workdir,
219
+ memory_limit_bytes=memory_limit_bytes,
220
+ partition_count=partition_count,
221
+ level=level + 1,
222
+ max_partition_depth=max_partition_depth,
223
+ )
224
+ return
225
+
226
+ yield from _block_hash_join(
227
+ left_path,
228
+ right_path,
229
+ left_key_fn,
230
+ right_key_fn,
231
+ memory_limit_bytes=memory_limit_bytes,
232
+ )
233
+
234
+
235
+ def _in_memory_join(
236
+ left_path: Path,
237
+ right_path: Path,
238
+ left_key_fn: Callable[[Record], Key],
239
+ right_key_fn: Callable[[Record], Key],
240
+ ) -> Iterator[tuple[Record, Record]]:
241
+ build_left = left_path.stat().st_size <= right_path.stat().st_size
242
+
243
+ if build_left:
244
+ build_path = left_path
245
+ probe_path = right_path
246
+ build_key_fn = left_key_fn
247
+ probe_key_fn = right_key_fn
248
+ else:
249
+ build_path = right_path
250
+ probe_path = left_path
251
+ build_key_fn = right_key_fn
252
+ probe_key_fn = left_key_fn
253
+
254
+ table: dict[Key, list[Record]] = {}
255
+
256
+ for record in _read_records(build_path):
257
+ table.setdefault(build_key_fn(record), []).append(record)
258
+
259
+ for probe_record in _read_records(probe_path):
260
+ matches = table.get(probe_key_fn(probe_record), ())
261
+
262
+ for build_record in matches:
263
+ if build_left:
264
+ yield build_record, probe_record
265
+ else:
266
+ yield probe_record, build_record
267
+
268
+
269
+ def _block_hash_join(
270
+ left_path: Path,
271
+ right_path: Path,
272
+ left_key_fn: Callable[[Record], Key],
273
+ right_key_fn: Callable[[Record], Key],
274
+ *,
275
+ memory_limit_bytes: int,
276
+ ) -> Iterator[tuple[Record, Record]]:
277
+ build_left = left_path.stat().st_size <= right_path.stat().st_size
278
+
279
+ if build_left:
280
+ build_path = left_path
281
+ probe_path = right_path
282
+ build_key_fn = left_key_fn
283
+ probe_key_fn = right_key_fn
284
+ else:
285
+ build_path = right_path
286
+ probe_path = left_path
287
+ build_key_fn = right_key_fn
288
+ probe_key_fn = left_key_fn
289
+
290
+ for chunk in _record_chunks(
291
+ build_path,
292
+ memory_limit_bytes=memory_limit_bytes,
293
+ ):
294
+ table: dict[Key, list[Record]] = {}
295
+
296
+ for record in chunk:
297
+ table.setdefault(build_key_fn(record), []).append(record)
298
+
299
+ for probe_record in _read_records(probe_path):
300
+ matches = table.get(probe_key_fn(probe_record), ())
301
+
302
+ for build_record in matches:
303
+ if build_left:
304
+ yield build_record, probe_record
305
+ else:
306
+ yield probe_record, build_record
307
+
308
+
309
+ def _partition_records(
310
+ records: Iterable[Record],
311
+ key_fn: Callable[[Record], Key],
312
+ *,
313
+ workdir: Path,
314
+ prefix: str,
315
+ partition_count: int,
316
+ level: int,
317
+ ) -> list[Path]:
318
+ paths = [
319
+ workdir / f"{prefix}-level-{level}-partition-{index}.bin"
320
+ for index in range(partition_count)
321
+ ]
322
+
323
+ files = [path.open("wb") for path in paths]
324
+
325
+ try:
326
+ for record in records:
327
+ partition = _partition_index(
328
+ key_fn(record),
329
+ partition_count=partition_count,
330
+ level=level,
331
+ )
332
+ _write_record(files[partition], record)
333
+ finally:
334
+ for file in files:
335
+ file.close()
336
+
337
+ return paths
338
+
339
+
340
+ def _partition_index(
341
+ key: Key,
342
+ *,
343
+ partition_count: int,
344
+ level: int,
345
+ ) -> int:
346
+ encoded = encode_key(key)
347
+ salt = level.to_bytes(4, byteorder="little", signed=False)
348
+ digest = hashlib.blake2b(
349
+ encoded + salt,
350
+ digest_size=8,
351
+ ).digest()
352
+
353
+ return (
354
+ int.from_bytes(
355
+ digest,
356
+ byteorder="little",
357
+ signed=False,
358
+ )
359
+ % partition_count
360
+ )
361
+
362
+
363
+ def _record_chunks(
364
+ path: Path,
365
+ *,
366
+ memory_limit_bytes: int,
367
+ ) -> Iterator[list[Record]]:
368
+ chunk: list[Record] = []
369
+ used_bytes = 0
370
+
371
+ for record in _read_records(path):
372
+ size = _serialized_size(record)
373
+
374
+ if chunk and used_bytes + size > memory_limit_bytes:
375
+ yield chunk
376
+ chunk = []
377
+ used_bytes = 0
378
+
379
+ chunk.append(record)
380
+ used_bytes += size
381
+
382
+ if used_bytes >= memory_limit_bytes:
383
+ yield chunk
384
+ chunk = []
385
+ used_bytes = 0
386
+
387
+ if chunk:
388
+ yield chunk
389
+
390
+
391
+ def _write_record(file: BinaryIO, record: Record) -> None:
392
+ data = record.data
393
+
394
+ if len(data) > 0xFFFFFFFF:
395
+ raise ValueError("record is too large for external-hash format")
396
+
397
+ file.write(struct.pack(_LENGTH_FORMAT, len(data)))
398
+ file.write(data)
399
+
400
+
401
+ def _read_records(path: Path) -> Iterator[Record]:
402
+ with path.open("rb") as file:
403
+ while True:
404
+ length_data = file.read(_LENGTH_SIZE)
405
+
406
+ if not length_data:
407
+ return
408
+
409
+ if len(length_data) != _LENGTH_SIZE:
410
+ raise ValueError("truncated external-hash record length")
411
+
412
+ length = struct.unpack(_LENGTH_FORMAT, length_data)[0]
413
+ data = file.read(length)
414
+
415
+ if len(data) != length:
416
+ raise ValueError("truncated external-hash record")
417
+
418
+ yield Record(data=data)
419
+
420
+
421
+ def _serialized_size(record: Record) -> int:
422
+ return _LENGTH_SIZE + len(record.data)
423
+
424
+
425
+ def _validate_parameters(
426
+ memory_limit_bytes: int,
427
+ partition_count: int,
428
+ max_partition_depth: int,
429
+ ) -> None:
430
+ if memory_limit_bytes <= 0:
431
+ raise ValueError("memory_limit_bytes must be positive")
432
+ if partition_count < 2:
433
+ raise ValueError("partition_count must be at least 2")
434
+ if max_partition_depth < 0:
435
+ raise ValueError("max_partition_depth must be non-negative")