megadbx 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. megadbx-1.0.0/PKG-INFO +1904 -0
  2. megadbx-1.0.0/README.md +1881 -0
  3. megadbx-1.0.0/megadbx/__init__.py +37 -0
  4. megadbx-1.0.0/megadbx/classes/__init__.py +0 -0
  5. megadbx-1.0.0/megadbx/classes/_timers.py +37 -0
  6. megadbx-1.0.0/megadbx/classes/append_only_log.py +49 -0
  7. megadbx-1.0.0/megadbx/classes/block_manifest.py +74 -0
  8. megadbx-1.0.0/megadbx/classes/bloom_filter.py +52 -0
  9. megadbx-1.0.0/megadbx/classes/composite_index_manager.py +224 -0
  10. megadbx-1.0.0/megadbx/classes/index_store.py +26 -0
  11. megadbx-1.0.0/megadbx/classes/key_cache.py +26 -0
  12. megadbx-1.0.0/megadbx/classes/lru.py +34 -0
  13. megadbx-1.0.0/megadbx/classes/mega_db.py +1968 -0
  14. megadbx-1.0.0/megadbx/classes/mega_db_error.py +9 -0
  15. megadbx-1.0.0/megadbx/classes/mega_db_full.py +452 -0
  16. megadbx-1.0.0/megadbx/classes/mega_db_safe.py +202 -0
  17. megadbx-1.0.0/megadbx/classes/merkle_manager.py +62 -0
  18. megadbx-1.0.0/megadbx/classes/mvcc_store.py +84 -0
  19. megadbx-1.0.0/megadbx/classes/queued_lock.py +23 -0
  20. megadbx-1.0.0/megadbx/classes/schema_validator.py +88 -0
  21. megadbx-1.0.0/megadbx/classes/shared_file_lock.py +201 -0
  22. megadbx-1.0.0/megadbx/classes/skiplist_index_manager.py +437 -0
  23. megadbx-1.0.0/megadbx/classes/transaction.py +294 -0
  24. megadbx-1.0.0/megadbx/classes/transaction_log.py +72 -0
  25. megadbx-1.0.0/megadbx/classes/trie_index_manager.py +284 -0
  26. megadbx-1.0.0/megadbx/classes/wal.py +79 -0
  27. megadbx-1.0.0/megadbx/panel/__init__.py +3 -0
  28. megadbx-1.0.0/megadbx/panel/admin_panel.py +500 -0
  29. megadbx-1.0.0/megadbx/panel/public/app.js +1089 -0
  30. megadbx-1.0.0/megadbx/panel/public/index.html +16 -0
  31. megadbx-1.0.0/megadbx/panel/public/style.css +970 -0
  32. megadbx-1.0.0/megadbx/utilities/__init__.py +0 -0
  33. megadbx-1.0.0/megadbx/utilities/defaults.py +30 -0
  34. megadbx-1.0.0/megadbx/utilities/functions.py +555 -0
  35. megadbx-1.0.0/megadbx.egg-info/PKG-INFO +1904 -0
  36. megadbx-1.0.0/megadbx.egg-info/SOURCES.txt +39 -0
  37. megadbx-1.0.0/megadbx.egg-info/dependency_links.txt +1 -0
  38. megadbx-1.0.0/megadbx.egg-info/requires.txt +12 -0
  39. megadbx-1.0.0/megadbx.egg-info/top_level.txt +1 -0
  40. megadbx-1.0.0/pyproject.toml +31 -0
  41. megadbx-1.0.0/setup.cfg +4 -0
megadbx-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,1904 @@
1
+ Metadata-Version: 2.4
2
+ Name: megadbx
3
+ Version: 1.0.0
4
+ Summary: Motor de base de datos embebida, rapida y robusta para Python.
5
+ Author: MegaStar
6
+ License: ISC
7
+ Keywords: database,db,embedded,json,megadb,jsondb
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
10
+ Classifier: Topic :: Database
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: xxhash
15
+ Requires-Dist: xxhash>=3.0.0; extra == "xxhash"
16
+ Provides-Extra: panel
17
+ Requires-Dist: flask>=2.0; extra == "panel"
18
+ Requires-Dist: bcrypt>=4.0; extra == "panel"
19
+ Provides-Extra: all
20
+ Requires-Dist: xxhash>=3.0.0; extra == "all"
21
+ Requires-Dist: flask>=2.0; extra == "all"
22
+ Requires-Dist: bcrypt>=4.0; extra == "all"
23
+
24
+ # MEGADBX (Python)
25
+
26
+ `megadbx` es un motor de base de datos embebida para Python, diseñado para ser rápido y confiable. Combina la flexibilidad de un **document store** con características avanzadas como integridad de datos, concurrencia segura, compresión, control de versiones, índices optimizados y alta robustez, todo en un paquete ligero y eficiente.
27
+
28
+ Este paquete es una adaptacion **completa y síncrona** del proyecto original `megadbx` para Node.js: misma lógica, mismos algoritmos, mismo formato de archivos en disco, la única diferencia real es que en node.js todo es `async/await` y aquí todo es **síncrono** (llamadas normales, sin `await`, sin loop de eventos).
29
+
30
+ ---
31
+ ## Características principales
32
+
33
+ - **Persistencia optimizada en archivos JSON comprimidos**
34
+ - Escritura atómica en disco con compresión opcional.
35
+ - Mirror y Journal para recuperación segura.
36
+
37
+ - **Integridad de datos avanzada**
38
+ - Hash **SHA-256** por bloque.
39
+ - **CRC32** por páginas de datos.
40
+ - **xxHash** para detección rápida de corrupción.
41
+ - **Checksum por documento** (no solo por bloque) si un documento puntual se corrompe, se detecta y se recupera solo, sin invalidar el resto del bloque (ver [Checksums por documento y recuperación automática](#checksums-por-documento-y-recuperación-automática)).
42
+ - **WAL con checksum por línea** detecta corrupción silenciosa (bitrot), no solo cortes a mitad de escritura.
43
+ - **Backups verificados**: `restoreBackup()` valida el checksum del backup ANTES de aplicar nada a la base de datos.
44
+
45
+ - **Compresión selectiva de campos** (`gzip` o `lz4`), con **umbral de tamaño mínimo** (`compressMinSize`) no comprime valores chicos donde comprimir sale más caro que dejarlos tal cual.
46
+
47
+ - **Búsquedas rápidas**
48
+ - **Bloom Filters** por bloque para aceleración de consultas.
49
+ - **LRU Cache** para acceso inmediato a datos recientes.
50
+ - Precarga de bloques en `find()`.
51
+
52
+ - **Automatización**
53
+ - `flush()` periódico configurable (corre solo, en un hilo de fondo).
54
+ - `createBackup()` automático con intervalos programados.
55
+
56
+ - **Snapshots & Backups**
57
+ - Crear y restaurar snapshots completos.
58
+ - Backups comprimidos con rotación flexible (por **contador** o **timestamp**), y **verificados por checksum antes de restaurar**.
59
+
60
+ - **Coordinación multiproceso** (`multiProcess: True`) para cuando varios procesos Python (por ejemplo, varios workers de `gunicorn`/`multiprocessing`) comparten la misma carpeta de base de datos: invalidación de caché entre procesos + locks por bloque, sin necesidad de un servidor central.
61
+
62
+ - **Transacciones ACID reales entre múltiples bases de datos** commit de 2 fases (prepare + apply) con recuperación automática si el proceso se cae a mitad de un commit.
63
+
64
+ - **TTL por documento** documentos que expiran solos, con expiración perezosa (`get()`) y un barrido activo periódico.
65
+
66
+ - **Modo seguro y multiproceso** (*MegaDB*, *MegaDBSafe*, *MegaDBFull*)
67
+ - **WAL (Write-Ahead Log)** para consistencia y recuperación.
68
+ - Concurrencia segura con **cola FIFO interna** (basada en `threading.RLock`).
69
+ - Consultas con filtros avanzados (`$gt`, `$lt`, `$in`, `$regex`, etc.).
70
+ - **Cursor de streaming** (`stream()`, `entries()`) generadores Python normales, para recorrer colecciones grandes sin cargarlas enteras en RAM.
71
+
72
+ - **Extensiones avanzadas** (*MegaDBFull*)
73
+ - Índices: secundarios, compuestos, **trie** (prefijos) y **skiplist** (rangos) todos con **modo paginado en disco opcional**, para colecciones grandes.
74
+ - `rebuildAllIndexes()` reconstruye todos los índices en streaming (memoria acotada), ideal para activarlos sobre datos que ya existían.
75
+ - **MVCC**: múltiples versiones de registros sin bloqueos pesados.
76
+ - **Append-only log (AOF)** para almacenamiento auditable.
77
+ - **Merkle Tree** para verificación criptográfica de integridad.
78
+
79
+ - **Schema opcional** (`required`, `type`, `enum`, `unique`) por colección.
80
+
81
+ - **Transacciones entre múltiples bases de datos.**
82
+ - **Panel de administración web** (`AdminPanel`, basado en Flask) login, CRUD completo con bloqueo optimista, gestión de índices/backups/transacciones pendientes, auditoría.
83
+ - **Visor de solo lectura** (`MegaDBVisual`, basado en Flask).
84
+ - ...y mucho más.
85
+ ---
86
+ ## Qué persistencia usa megadbx actualmente al guardar los datos
87
+
88
+ megadbx implementa un sistema de persistencia diseñado para garantizar integridad de datos, resistencia a fallos y consistencia en disco, incluso en casos de caída del proceso o del sistema operativo.
89
+
90
+ El ciclo de escritura y persistencia combina varias capas de protección:
91
+
92
+ ### 1. Escritura atómica
93
+ Cada modificación en un bloque se guarda mediante un proceso *atomic write*:
94
+ - Se escribe primero en un archivo temporal.
95
+ - El archivo temporal se sincroniza en disco (`fsync`).
96
+ - Luego se reemplaza el archivo original (`os.replace`), operación atómica en sistemas POSIX.
97
+
98
+ Esto asegura que nunca exista un archivo a medio escribir: o queda el archivo anterior, o el nuevo completo.
99
+
100
+ ### 2. Journal de respaldo
101
+ Antes de sobrescribir un bloque, megadbx crea un archivo journal (`.journal`) que contiene la nueva versión.
102
+ - Si ocurre un fallo en mitad de la escritura, el journal queda disponible como copia.
103
+ - Una vez que la escritura finaliza con éxito, el journal se elimina.
104
+
105
+ Este patrón es similar al *Write-Ahead Logging (WAL)*.
106
+
107
+ ### 3. Mirror de bloques
108
+ Además del archivo principal, cada bloque se replica en un directorio espejo (mirror).
109
+ - El mirror mantiene una segunda copia sincronizada de cada bloque.
110
+ - Si el archivo principal se corrompe (ejemplo: fallo de disco, corte de energía durante flush), megadbx puede recuperarlo automáticamente desde el mirror en el próximo arranque.
111
+
112
+ ### 4. Checksums e integridad
113
+ Cada bloque almacenado en disco incluye:
114
+ - `crcPages`: sumas CRC32 por páginas, para verificar integridad parcial.
115
+ - `xxhash`: un hash fuerte del bloque completo.
116
+ - `keyCrc`: checksums de subdocumentos individuales.
117
+ - `docChecksums`: un checksum **por documento raíz** (no solo del bloque entero) ver la sección siguiente.
118
+
119
+ Con esto se puede detectar y reparar corrupción de datos a varios niveles.
120
+
121
+ ### Checksums por documento y recuperación automática
122
+ Además del checksum de bloque completo, cada documento raíz tiene su propio checksum guardado junto a el (`docChecksums`). Esto importa porque un checksum de bloque entero solo detecta que "algo en este bloque cambió" mas no te dice *que* documento, y una corrupción de un solo documento invalidaría la lectura de todos los demás que viven en el mismo bloque si solo tuvieras el checksum global.
123
+
124
+ Cuando `get()` detecta que el checksum de un documento no coincide, dispara una cadena de recuperación en este orden, probando cada fuente hasta encontrar una copia íntegra:
125
+
126
+ 1. **Espejo (mirror)** la copia espejo del bloque, escrita de forma atómica e independiente. Cubre el caso más común: bitrot de disco en una sola de las dos copias.
127
+ 2. **Backups** del más reciente al más viejo, cada uno con su propio checksum verificado antes de confiar en él. Se extrae solo el documento puntual, no se restaura la colección entera.
128
+ 3. **WAL** disponible en `MegaDBSafe`/`MegaDBFull`: se reconstruye el último valor conocido reaplicando en orden las operaciones registradas para esa clave.
129
+ 4. **Cuarentena** si ninguna fuente tenía una copia íntegra, la copia cruda (posiblemente corrupta) se mueve a una carpeta `_quarantine/` y lanza un `MegaDBError` explícito, en vez de devolver datos potencialmente corruptos silenciosamente.
130
+
131
+ ### Índice global
132
+ - `useGlobalIndex: True` mantiene un archivo `index.json` (mapa clave raíz => bloque) para acelerar la carga tras reiniciar.
133
+ - Si `index.json` falta o está dañado, megadbx puede reconstruir el índice completo a partir de los datos de los bloques.
134
+
135
+ ---
136
+ En conjunto, estas técnicas permiten:
137
+ - **Atomicidad**: nunca quedan datos parciales.
138
+ - **Durabilidad**: los cambios confirmados sobreviven a fallos del proceso o del sistema operativo.
139
+ - **Recuperación rápida**: con journal y mirror, la base de datos se repara sola si encuentra corrupción.
140
+ - **Seguridad extra**: snapshots y backups permiten volver atrás en caso de error humano.
141
+ - Evita lecturas repetitivas de JSON en disco.
142
+ - Mantiene la mayoría de las operaciones de consulta en memoria.
143
+ - Ofrece tiempos de respuesta consistentes incluso en bases con muchos datos.
144
+ ---
145
+
146
+ ## Instalación
147
+
148
+ ```bash
149
+ pip install megadbx
150
+
151
+ # Opcional, para checksums xxh64 (si no está, cae a sha256 truncado):
152
+ pip install megadbx[xxhash]
153
+
154
+ # Opcional, si usas compressAlg='lz4':
155
+ pip install lz4
156
+
157
+ # Opcional, si usas la clase AdminPanel (ver sección más abajo):
158
+ pip install megadbx[panel]
159
+
160
+ # Opcional, si usas la clase MegaDBVisual:
161
+ pip install megadbx[visual]
162
+ ```
163
+
164
+ ---
165
+
166
+ # Notas generales sobre parámetros y uso de palabras.
167
+ * En la documentacion se usan palabras como:
168
+ - `coleccion`: Toda la base de datos (db) => {key1: value1, key2: value2, etc} => {documento1, documento2, etc}
169
+ - `documento(s)`: Lo que se guardó en la base de datos => {key: value}
170
+ - `clave/key`: La clave con la que se guardo el documento => key
171
+ - `value/valor`: El valor guardado en el documento => value
172
+ - `query`: diccionario de filtro usando operadores relacionales => { "edad": { "$gt": 18 } } etc.
173
+ - `path`: Ruta dentro del documento ("usuario1") o coleccion (si usa wildcard)
174
+ - `wildcard`: Símbolo especial para acceder a toda la coleccion o sub documentos de un documento. "*"
175
+ - `dot notation`: Símbolo especial para acceder anidadamente a una ruta dentro de un documento '.'
176
+
177
+ > **Nota sobre `None` vs valores ausentes**: JavaScript distingue `undefined` (ausencia de valor) de `null` (valor nulo explícito). Python no tiene un equivalente nativo de `undefined`, así que en este port **ambos casos se representan con `None`** el comportamiento esperable de un `.get()` de Python sobre una clave inexistente.
178
+
179
+ # Operadores relacionales y textuales
180
+
181
+ * Existen operadores que nos ayudan a la hora de filtrar o buscar documentos, estos operadores siguen una regla estricta:
182
+ - `$gt`: Mayor que: **valor > $gt**
183
+ - `$gte`: Mayor o igual que: **valor >= $gte**
184
+ - `$lt`: Menor que: **valor < $lt**
185
+ - `$lte`: Menor o igual que: **valor <= $lte**
186
+ - `$eq`: Igualdad: **valor == $eq**
187
+ - `$ne`: Distinto: **valor != $ne**
188
+ - `$in`: Pertenece a la lista - $in debe ser una lista: **valor in $in**
189
+ - `$nin`: No pertenece a la lista - $nin debe ser una lista: **valor not in $nin**
190
+ - `$between`: Rango inclusivo [min, max] - Esta dentro del rango min y max: **valor >= min and valor <= max**
191
+ - `$prefix`: Coincidencia de prefijo de string: **str(valor).startswith(...)**
192
+ - `$regex`: Expresión regular (string), opcional `$flags` (`i`, `s`, `m`): **re.search($regex, str(valor), flags)**
193
+
194
+
195
+ # Clases principales
196
+
197
+ ## (MegaDB) Constructor y métodos:
198
+ * [MegaDB](#megadb)
199
+ * [set](#set)
200
+ * [has](#has)
201
+ * [get](#get)
202
+ * [delete](#delete)
203
+ * [all](#all)
204
+ * [stream](#stream)
205
+ * [entries](#entries)
206
+ * [keys](#keys)
207
+ * [values](#values)
208
+ * [count](#count)
209
+ * [find](#find)
210
+ * [aggregate](#aggregate)
211
+ * [update](#update)
212
+ * [watch](#watch)
213
+ * [stats](#stats)
214
+ * [flush](#flush)
215
+ * [close](#close)
216
+ * [createBackup](#createbackup)
217
+ * [restoreBackup](#restorebackup)
218
+ * [listBackups](#listbackups)
219
+ * [createSnapshot](#createsnapshot)
220
+ * [restoreSnapshot](#restoresnapshot)
221
+ * Conceptos relacionados:
222
+ * [Schema](#schema)
223
+ * [Umbral de compresión](#umbral-de-compresión)
224
+ * [Multiproceso](#multiproceso)
225
+ * [TTL por documento](#ttl-por-documento)
226
+ * [Checksums por documento y recuperación automática](#checksums-por-documento-y-recuperación-automática)
227
+ ---
228
+ ## (MegaDBSafe) Constructor y métodos:
229
+ * [MegaDBSafe](#megadbsafe)
230
+ * [ready](#ready)
231
+ * [set](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
232
+ * [has](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
233
+ * [get](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
234
+ * [delete](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
235
+ * [all](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
236
+ * [stream](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
237
+ * [entries](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
238
+ * [keys](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
239
+ * [values](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
240
+ * [count](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
241
+ * [find](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
242
+ * [aggregate](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
243
+ * [update](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
244
+ * [watch](#set--has--get--delete--all--stream--entries--keys--values--count--find--aggregate--update--watch)
245
+ * [stats](#stats-1)
246
+ * [flush](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
247
+ * [close](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
248
+ * [createBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
249
+ * [restoreBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
250
+ * [listBackups](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
251
+ * [createSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
252
+ * [restoreSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot)
253
+ ---
254
+ ## (MegaDBFull) Constructor y métodos:
255
+ * [MegaDBFull](#megadbfull)
256
+ * [ready](#ready-1)
257
+ * [set](#set-1)
258
+ * [has](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
259
+ * [get](#get-1)
260
+ * [delete](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
261
+ * [all](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
262
+ * [stream](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
263
+ * [entries](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
264
+ * [keys](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
265
+ * [values](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
266
+ * [count](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
267
+ * [find](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
268
+ * [aggregate](#has--all--stream--entries--delete--keys--values--count--find--aggregate)
269
+ * [update](#update-1)
270
+ * [watch](#watch-1)
271
+ * [history](#history)
272
+ * [stats](#stats-2)
273
+ * [flush](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
274
+ * [close](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
275
+ * [createBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
276
+ * [restoreBackup](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
277
+ * [listBackups](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
278
+ * [createSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
279
+ * [restoreSnapshot](#flush--close--createbackup--restorebackup--listbackups--createsnapshot--restoresnapshot-1)
280
+ * [verifyIntegrityFast](#verifyintegrityfast)
281
+ * [rebuildAllIndexes](#rebuildallindexes)
282
+ * Conceptos relacionados:
283
+ * [Índices paginados en disco](#índices-paginados-en-disco-compositeindexmanager-trieindexmanager-y-skiplistindexmanager)
284
+ ---
285
+ ## (Transaction) Constructor y métodos:
286
+ * [Transaction](#transaction)
287
+ * `tx.<db>.get(path)`
288
+ * `.set(path, value)`
289
+ * `.has(path)`
290
+ * `.delete(path)`
291
+ * [commit](#commit)
292
+ * [rollback](#rollback)
293
+ * [Transaction.recoverPending](#transactionrecoverpending-estático)
294
+ ---
295
+ ## (AdminPanel) Constructor y métodos:
296
+ * [AdminPanel](#adminpanel)
297
+ * [start](#start)
298
+ * [stop](#stop)
299
+ ---
300
+
301
+ ## Dato importante, [leer esto después de entender cómo funciona MegaDB/MegaDBSafe/MegaDBFull](#leer-después-de-entender-megadb-megadbsafe-y-megadbfull)
302
+
303
+
304
+
305
+ # 1. `MegaDB`
306
+
307
+ ## MegaDB
308
+ ```python
309
+ from megadbx import MegaDB
310
+ db = MegaDB(collection_name, options={})
311
+ ```
312
+
313
+ - **collection_name** (str): nombre de la colección (carpeta en `./db/`).
314
+ - **options** (dict, opcional):
315
+
316
+
317
+ | Opción | Default | Explicación |
318
+ | ---------------------- | ---------------------------------- | ----------------------------------------------------------|
319
+ | `dir` | directorio del script principal | Directorio donde se creará la carpeta del db/ |
320
+ | `mirrorDirName` | `mirror` | Carpeta para guardar los 'espejos' del db |
321
+ | `blockSize` | `100` | Cantidad de claves por bloque. |
322
+ | `flushInterval` | `30000` ms | Cada cuánto guardar datos a disco. |
323
+ | `crcPageSize` | `4096` bytes | Tamaño de página para CRC. |
324
+ | `schema` | `None` | Validación (`required`, `type`, `enum`, `unique`) ver [Schema](#schema). |
325
+ | `secondaryIndexes` | `[]` | Campos indexados. |
326
+ | `rebuildIndexOnLoad` | `False` | Reconstruye los campos indexados al inicializar el db. |
327
+ | `compressFields` | `[]` | Campos a comprimir. |
328
+ | `compressMinSize` | `256` bytes | No comprime valores por debajo de este tamaño, ver [Umbral de compresión](#umbral-de-compresión). |
329
+ | `bloomK` | `4` | Hashes en BloomFilter. |
330
+ | `expectedKeysPerBlock` | `1000` | Estimación de cuántas claves tendrá cada bloque. |
331
+ | `bloomM` | `expectedKeysPerBlock * 10` | Tamaño (en bits) del Bloom filter por bloque. |
332
+ | `lruCap` | `128` | Tamaño de caché LRU. |
333
+ | `integrityOnRead` | `True` | Verificar integridad en lectura. |
334
+ | `integrityCooldownMs` | `180000` ms | Tiempo entre verificaciones de integridad. |
335
+ | `backupMode` | `counter` | Nombres de backup (`counter` o `timestamp`). |
336
+ | `backupInterval` | `3600000` ms | Intervalo entre backups. |
337
+ | `maxBackups` | `10` | Backups a conservar. |
338
+ | `compressAlg` | `gzip` | Algoritmo de compresión (`gzip` o `lz4`). |
339
+ | `useGlobalIndex` | `False` | Mantiene y persiste un índice global simple. |
340
+ | `indexFlushInterval` | `1800000` ms | Cada cuánto se guarda cuando `useGlobalIndex` está en `True`. |
341
+ | `keyCacheCap` | `1024` | Tamaño de una caché de claves-valor para acelerar `get()`. |
342
+ | `multiProcess` | `False` | Coordinación entre procesos que comparten la misma carpeta `db/`, ver [Multiproceso](#multiproceso). |
343
+ | `manifestPollIntervalMs` | `3000` ms | Cada cuánto revisa el manifiesto de versiones en modo `multiProcess`. |
344
+ | `lockTimeoutMs` | `30000` ms | Tiempo máximo esperando un lock de bloque en modo `multiProcess`. |
345
+ | `lockTtlMs` | `30000` ms | TTL del lock de escritura (para detectar locks huérfanos si un proceso muere sosteniéndolo). |
346
+ | `ttlSweepIntervalMs` | `flushInterval` | Cada cuánto corre el barrido activo de documentos vencidos, ver [TTL por documento](#ttl-por-documento). |
347
+
348
+
349
+ **(Explicación detallada)**
350
+
351
+ `dir`: Directorio donde se creará la base de datos; la carpeta será `[dir]/db/[collection_name]`. Si no se especifica, se usa el directorio del script principal que arrancó el proceso Python.
352
+ ```python
353
+ from megadbx import MegaDB
354
+ import os
355
+
356
+ """
357
+ Estructura actual de la carpeta global:
358
+ (estoy usando el archivo db.py)
359
+
360
+ 📂 proyecto
361
+ 📂 base_de_datos
362
+ 📄 handler.py
363
+ 📄 db.py
364
+ 📂 comandos
365
+ 📄 comando1.py
366
+ """
367
+
368
+ # Crear la db a la altura de la carpeta base_de_datos/comandos.
369
+ db = MegaDB('usuarios') # sin opciones, usa el default.
370
+ """
371
+ 📂 proyecto
372
+ 📂 base_de_datos
373
+ 📄 handler.py
374
+ 📄 db.py
375
+ 📂 comandos
376
+ 📄 comando1.py
377
+ 📂 db
378
+ 📂 usuarios
379
+ """
380
+ # o también usando dir explícito:
381
+ db = MegaDB('usuarios', {
382
+ "dir": os.path.join(os.path.dirname(__file__), "..")
383
+ })
384
+ ```
385
+
386
+ `mirrorDirName`: Carpeta donde se crean los respaldos "espejos", para recuperar archivos si algo falla al escribir (default: `'mirror'`).
387
+
388
+ `blockSize`: Tamaño de bloque en cantidad de claves (default: 100).
389
+ - Divide los datos en varios bloques para no tener un JSON gigante.
390
+ - **megadbx** no guarda toda la colección en un solo archivo (como haría un `usuarios.json` enorme).
391
+ - En lugar de eso, divide la colección en varios bloques (archivos pequeños).
392
+ - **blockSize** define el número aproximado de claves que se guardan en cada bloque.
393
+ - Por defecto es 100: cada archivo (`block_0001.json`, `block_0002.json`, etc.) contendrá alrededor de 100 claves.
394
+ ```python
395
+ # Supongamos que guardas 500 usuarios en la colección "usuarios":
396
+ for i in range(1, 501):
397
+ db.set(f"u{i}", {"nombre": f"Persona{i}"})
398
+ ```
399
+ ```
400
+ Caso 1 — blockSize: 100 (default)
401
+ db/usuarios/
402
+ ├── block_0001.json ← contiene ~100 usuarios
403
+ ├── block_0002.json ← contiene ~100 usuarios
404
+ ├── block_0003.json ← contiene ~100 usuarios
405
+ ├── block_0004.json ← contiene ~100 usuarios
406
+ ├── block_0005.json ← contiene ~100 usuarios
407
+ ```
408
+ - 500 usuarios = 5 bloques.
409
+ - Bloques más pequeños → más archivos, más fragmentación.
410
+ - Ideal si quieres minimizar pérdidas (solo se daña un bloque chiquito).
411
+
412
+ Valores pequeños (ej. 50):
413
+ - ✅ Más seguro (si se daña un bloque, pierdes menos).
414
+ - ❌ Más archivos que manejar, un poco más lento para recorrer todo.
415
+
416
+ Valores grandes (ej. 500 o 1000):
417
+ - ✅ Menos archivos, búsquedas más rápidas en algunos casos.
418
+ - ❌ Si se corrompe un bloque, pierdes más datos.
419
+
420
+ Valor por defecto (100):
421
+ - Equilibrio entre seguridad y rendimiento.
422
+
423
+ No te preocupes: hay métodos avanzados de seguridad/respaldo, tanto internos automáticos como otros a tu criterio, para reducir esto prácticamente a cero.
424
+
425
+ `flushInterval`: Cada cuánto (ms) se guardan los cambios automáticamente en disco (se escriben a disco los cambios pendientes) en segundo plano.
426
+ - Más corto = menos pérdida posible si se cae el proceso, pero más I/O.
427
+ - Default: 30000 ms => 30s.
428
+ - Los valores se reflejan en disco al hacer flush; en memoria se actualiza en tiempo real.
429
+ - Internamente corre en un hilo daemon (`threading.Timer`), sin que tengas que hacer nada.
430
+
431
+ `crcPageSize`: Tamaño de "página" para calcular CRC por fragmentos del archivo; ayuda a detectar corrupción y reparar desde el espejo (mirror) (default: 4096 = 4KB).
432
+
433
+ ## Schema
434
+ `schema`: Validación opcional por colección al hacer `set()`.
435
+
436
+ ```python
437
+ db = MegaDB("usuarios", {
438
+ "schema": {
439
+ "fields": {
440
+ "nombre": {"type": "string", "required": True},
441
+ "edad": {"type": "number"},
442
+ "email": {"type": "string", "required": True, "unique": True},
443
+ "rol": {"type": "string", "enum": ["admin", "user", "guest"]},
444
+ }
445
+ }
446
+ })
447
+
448
+ db.set("u1", {"nombre": "dix", "edad": 30, "email": "dix@x.com"}) # OK
449
+
450
+ db.set("u2", {"edad": 20})
451
+ # lanza MegaDBError: Documento invalido para "u2": campo requerido
452
+ # faltante: "nombre"; campo requerido faltante: "email"
453
+
454
+ db.set("u3", {"nombre": "Bob", "email": "dix@x.com"})
455
+ # lanza MegaDBError: Valor duplicado para campo unico "email" en "u3": 'dix@x.com'
456
+ ```
457
+
458
+ Reglas soportadas por campo:
459
+ - `type`: `'string' | 'number' | 'boolean' | 'object' | 'array'`.
460
+ - `required`: si `True`, el campo no puede faltar.
461
+ - `enum`: lista de valores permitidos.
462
+ - `unique`: no puede haber dos documentos con el mismo valor en ese campo. El índice de unicidad se reconstruye solo (escaneando lo existente una vez) la primera vez que se necesita, así que también funciona si activas `unique` sobre datos que ya tenías guardados.
463
+
464
+ **Compatibilidad**: si le pasas el formato "plano" (`{campo: 'tipo'}`), se normaliza automáticamente al formato con `fields`:
465
+ ```python
466
+ # formato plano, también funciona (se normaliza solo):
467
+ schema = {"edad": "number", "nombre": "string"}
468
+ ```
469
+
470
+ **Alcance (a propósito, para mantenerlo simple y predecible)**: solo valida cuando se escribe un documento RAÍZ completo (`db.set('u1', {...})`), no en escrituras de rutas anidadas (`db.set('u1.direccion.ciudad', 'Lima')`). Validar en escrituras parciales requeriría leer + mergear + revalidar el documento entero en cada escritura anidada.
471
+
472
+ En `MegaDBSafe`/`MegaDBFull`, la validación corre **antes** de tocar el WAL/AOF/MVCC, si el documento es inválido, la operación nunca llega a loguearse.
473
+
474
+ `secondaryIndexes`: Lista de campos para indexar y acelerar búsquedas por igualdad en `find()`, usando filtros `$eq`/`$in`/`$ne`/`$nin`/`$gt`/`$lt`/`$gte`/`$lte`/`$between`/`$prefix`/`$regex` sobre el valor.
475
+
476
+ - Guarda en un diccionario inverso las rutas de las claves asociadas a un valor.
477
+ - Esto evita tener que recorrer todos los datos ya que directamente accede a la ruta.
478
+ - Soporta rutas anidadas usando *dot notation* (el punto `.`).
479
+ - Muy útil cuando tienes miles de datos. Ej:
480
+
481
+ ```python
482
+ # Este ejemplo fue testeado con 50,000 usuarios añadidos al db
483
+
484
+ # usando propiedades planas al igual que dot notation
485
+ secondary_indexes = ["nombre", "perfil.id", "perfil.edad"]
486
+ db = MegaDB("usuarios", {"secondaryIndexes": secondary_indexes})
487
+
488
+ db.set("u1", {"nombre": "mega", "email": "mega@gmail.com"})
489
+ db.set("u2", {"nombre": "ratsa", "email": "ratsa@gmail.com"})
490
+
491
+ db.set("u3", {"perfil": {"id": "dix", "edad": 25}})
492
+ db.set("u4", {"perfil": {"id": "Luis", "edad": 30}})
493
+ db.set("u5", {"perfil": {"id": "dix", "edad": 40}})
494
+
495
+ # usamos find sin el beneficio del indice (comparación conceptual):
496
+ import time
497
+ t0 = time.perf_counter()
498
+ data = db.find("*", {"nombre": {"$eq": "mega"}})
499
+ print(time.perf_counter() - t0)
500
+ print(data) # [{'nombre': 'mega', 'email': 'mega@gmail.com'}]
501
+
502
+ t0 = time.perf_counter()
503
+ data2 = db.find("*", {"perfil.id": {"$eq": "dix"}})
504
+ print(time.perf_counter() - t0)
505
+ print(data2)
506
+ # [{'perfil': {'id': 'dix', 'edad': 25}}, {'perfil': {'id': 'dix', 'edad': 40}}]
507
+ ```
508
+ **DATO IMPORTANTE:**
509
+
510
+ *En MegaDB:*
511
+
512
+ - `secondaryIndexes`, al guardar en memoria, solo guarda los datos que se hicieron posteriores a la inicialización del db, si tenías datos agregados anteriormente, solo se indexan los nuevos hasta que el db se reinicie (desventaja por tenerlo solo en memoria).
513
+ - Existe la opción de revertir esto: al iniciar el db, se cargan todos los datos y se reconstruye el `secondaryIndexes` en memoria en base a tu db actual (ver `rebuildIndexOnLoad`).
514
+
515
+ *En [MegaDBSafe](#megadbsafe) o [MegaDBFull](#megadbfull)*
516
+
517
+ - `secondaryIndexes` persiste en disco (se guarda en `indexes.json`), así que al iniciar el db solo se toman los datos actuales del archivo, mejorando flexibilidad y rapidez.
518
+ - Para sincronizar datos preexistentes con `indexes.json`, usa `rebuildIndexOnLoad`, puedes usarlo una vez para sincronizar y luego deshabilitarlo.
519
+
520
+ `rebuildIndexOnLoad`: Reconstruye los campos indexados del `secondaryIndexes` tomando los datos ya existentes de los bloques (default: `False`).
521
+
522
+ `compressFields`: Lista de rutas de campos (soporta dot notation `.` y wildcards `*`) a comprimir con gzip o lz4 al persistir en disco. Al leer se descomprime solo. Útil para textos grandes:
523
+ ```python
524
+ db = MegaDB("datos", {"compressFields": [
525
+ "user1", # campo top-level, comprime todo el subárbol
526
+ "user2.descripcion", # anidado dot notation .
527
+ "*.descripcion", # wildcard * , aplica a todas las claves raíz con subcampo descripcion
528
+ "datos.*.prof", # dot notation + wildcard, aplica a todas las claves de datos con subcampo prof
529
+ ]})
530
+
531
+ db.set('user1', {"titulo": "Ejemplo", "descripcion": "Texto muuuuuuuy largo....."})
532
+ db.set('user2', {"titulo": "Ejemplo", "descripcion": "Texto muuuuuuuy largo....."})
533
+
534
+ db.set('datos', {
535
+ "mario": {"prof": "programador", "edad": 20},
536
+ "pedro": {"prof": "agronomo", "edad": 25},
537
+ "juan": False,
538
+ }) # aplica "datos.*.prof" a mario y pedro
539
+ ```
540
+ - Puedes combinar wildcards y dot notation incluso más de una vez: `data.*.*.obj1`, `*.*.obj2`, etc.
541
+
542
+ `bloomK`: Número de hashes del Bloom filter por bloque. Más alto = menos falsos positivos, más CPU (default: 4).
543
+
544
+ `expectedKeysPerBlock`: Estimación de cuántas claves tendrá cada bloque; se usa para dimensionar el Bloom filter si no fijas `bloomM` (default: 1000).
545
+
546
+ `bloomM`: Tamaño (en bits) del Bloom filter por bloque; si no se pasa, se calcula como `expectedKeysPerBlock * 10`.
547
+
548
+ - Tip rápido: si quieres bajar falsos positivos, sube `bloomM` o `bloomK`.
549
+
550
+ `integrityOnRead`: Activa verificaciones de integridad (CRC por páginas + hash) de forma periódica/al leer los bloques. Si detecta corrupción intenta auto-recuperar desde el `mirror` (default: `True`).
551
+
552
+ `integrityCooldownMs`: Tiempo mínimo entre verificaciones de integridad (default: 180000 ms => 3 minutos, para no "castigar" el disco).
553
+
554
+ `backupMode`: `"counter"` o `"timestamp"`. Define cómo nombrar los backups.
555
+
556
+ `backupInterval`: Cada cuánto (ms) se crea un backup automático comprimido de todos los bloques, rotado según `maxBackups` (default: 1 hora).
557
+
558
+ `maxBackups`: Cuántos backups guardar antes de borrar los viejos (default: 10).
559
+
560
+ `compressAlg`: Algoritmo de compresión para bloques/campos, y también para snapshots/backups (default: `gzip`).
561
+
562
+ | Característica | **Gzip** | **LZ4** |
563
+ | ------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ |
564
+ | **Compresión** | Alta (archivos más pequeños) | Media (archivos más grandes que gzip) |
565
+ | **Velocidad de compresión** | Lenta (más CPU) | Muy rápida |
566
+ | **Velocidad de descompresión** | Buena, pero más lenta que LZ4 | Extremadamente rápida |
567
+ | **CPU requerida** | Mayor | Mucho menor |
568
+ | **Soporte nativo en Python** | Sí (`gzip`, stdlib) | No (se instala `lz4`) |
569
+ | **Instalación** | No requiere nada | `pip install lz4` (puede requerir compilador C) |
570
+
571
+ `useGlobalIndex`: Mantiene y persiste un índice global simple en `index.json` (mapea claves raíz → bloque), para acelerar la carga/consultas tras reiniciar (default: `False`).
572
+
573
+ `indexFlushInterval`: Cada cuánto se persiste `index.json` cuando `useGlobalIndex` está en `True`; si lo pones en 0 no hay intervalo periódico (default: 1800000 ms => 30 minutos).
574
+
575
+ `lruCap`: Capacidad de la caché LRU de bloques en memoria (default: 128); más grande = menos lecturas de disco, más RAM.
576
+
577
+ `keyCacheCap`: Tamaño máximo de la caché LRU para claves individuales, controla cuántas claves recientes se guardan en memoria para acelerar lecturas repetidas (default: 1024). Cuando llamas a `get(path)`, la base de datos guarda en memoria los últimos resultados para que futuras lecturas sean instantáneas, sin:
578
+ - Leer el bloque desde disco.
579
+ - Descomprimir el campo (si estaba en `compressFields`).
580
+ - Pasar por validaciones de integridad.
581
+
582
+ ```python
583
+ db = MegaDB("users", {"keyCacheCap": 2}) # solo guardará 2 claves en caché
584
+
585
+ db.set("user.1", {"name": "mega", "age": 13})
586
+ db.set("user.2", {"name": "ratsa", "age": 14})
587
+ db.set("user.3", {"name": "Charlie", "age": 40})
588
+
589
+ print(db.get("user.1")) # primera lectura: va a disco y guarda en caché
590
+ print(db.get("user.1")) # segunda lectura: desde caché, mucho más rápida
591
+ print(db.get("user.3")) # al llegar a 3 claves, la más antigua se expulsa
592
+ ```
593
+
594
+ ## Umbral de compresión
595
+ `compressMinSize`: Con `compressFields`, no comprime valores cuyo tamaño serializado (JSON) esté por debajo de este umbral (default: 256 bytes). Comprimir un valor chico es contraproducente: el header de gzip por sí solo son ~18 bytes, y sumale el costo de CPU, para valores chicos, terminas gastando más de lo que ahorras.
596
+
597
+ ```python
598
+ db = MegaDB("usuarios", {
599
+ "compressFields": ["bio"],
600
+ "compressMinSize": 256, # default
601
+ })
602
+
603
+ db.set("u1", {"bio": "hola"}) # ~15 bytes: NO se comprime
604
+ db.set("u2", {"bio": "x" * 2000}) # 2000+ bytes: SI se comprime
605
+ ```
606
+
607
+ ## Multiproceso
608
+ `multiProcess`: Coordina esta instancia con OTROS procesos Python que comparten la misma carpeta `db/`, el caso típico son varios workers de un servidor WSGI (`gunicorn`, `uwsgi`) o `multiprocessing`, donde cada worker es un proceso de sistema operativo separado con su propia copia en memoria de `blocksCache` (default: `False`).
609
+
610
+ **El problema que resuelve:** worker A escribe un documento, worker B tiene ese mismo bloque cacheado desde antes en su memoria, sin coordinación, worker B seguiría devolviendo el valor viejo indefinidamente.
611
+
612
+ **Cómo lo resuelve:**
613
+ 1. **`BlockManifest`** un archivo chico (`_manifest.json`) con `{blockId: version}`. Cada escritura exitosa incrementa la versión de ESE bloque. Los demás procesos hacen polling periódico (`manifestPollIntervalMs`) e invalidan solo los bloques que realmente cambiaron.
614
+ 2. **`SharedFileLock`** un lock de lectores/escritor **por bloque** (no global), basado en archivos-marcador con heartbeat/TTL. Se adquiere antes de escribir un bloque en disco.
615
+
616
+ ```python
617
+ db = MegaDB("pedidos", {
618
+ "dir": "./",
619
+ "multiProcess": True, # activa la coordinacion
620
+ "manifestPollIntervalMs": 3000, # default
621
+ "lockTimeoutMs": 30000, # default
622
+ "lockTtlMs": 30000, # default
623
+ })
624
+ ```
625
+
626
+ **Costo si NO lo necesitas:** cero, con `multiProcess: False` (default) no se instancia ningún manifiesto ni lock. Solo actívalo si de verdad tienes más de un proceso tocando la misma carpeta `db/` al mismo tiempo. Para concurrencia entre **hilos** dentro de un mismo proceso, no hace falta `multiProcess`: eso ya lo cubre el lock interno (`QueuedLock`, basado en `threading.RLock`) de `MegaDBSafe`/`MegaDBFull`.
627
+
628
+ ## TTL por documento
629
+ Documentos que expiran solos, pasado un tiempo. Se define por escritura, no por colección, ver [`set(path, value, opts)`](#set) para el parámetro `ttl`.
630
+
631
+ ```python
632
+ db = MegaDB("sesiones", {"ttlSweepIntervalMs": 60000}) # default: usa flushInterval (30000ms)
633
+
634
+ db.set("s1", {"userId": "u1"}, {"ttl": 60000}) # expira en 60s
635
+ db.set("s2", {"userId": "u2"}) # sin ttl, permanente
636
+
637
+ print(db.get("s1")) # {'userId': 'u1'}
638
+ # ... pasan 60+ segundos ...
639
+ print(db.get("s1")) # None — expiro
640
+ ```
641
+
642
+ **Cómo se expira:**
643
+ - **Perezosa (lazy)**: si alguien pide una clave vencida vía `get()`, se borra ahí mismo y devuelve `None`.
644
+ - **Barrido activo**: cada `ttlSweepIntervalMs`, se revisan los bloques "tibios" en memoria y se borran los documentos vencidos que nadie volvió a leer.
645
+
646
+ > Mismo alcance que `schema`: el TTL aplica a documentos raíz completos. Si haces `set()` de una ruta anidada, no se le asigna TTL (y si el documento raíz ya tenía uno, un `set()` posterior sin `ttl` se lo quita).
647
+
648
+ ## set
649
+ ### `set(path, value, opts={})`
650
+ Crea o actualiza un valor en la ubicación indicada por `path`.
651
+
652
+ - **`path` (str):** ruta jerárquica donde almacenar el valor, soporta dot notation `.`
653
+ Ejemplo: `"users.1.name"` → dentro de `users`, clave `1`, propiedad `name`.
654
+ - **`value` (any):** el valor que quieres guardar (str, número, bool, dict, lista, etc.).
655
+ - **`opts` (dict, opcional):**
656
+ - `ttl` (número, ms): si se pasa, el documento raíz expira solo después de ese tiempo, ver [TTL por documento](#ttl-por-documento). Solo aplica cuando `path` es un documento raíz (sin dot notation).
657
+ - **`retorna`:** `True` si se guardó correctamente.
658
+
659
+ Si tienes un `schema` definido en las opciones del constructor, `set` lo valida antes de guardar.
660
+
661
+ ```python
662
+ db.set("users.1", {"name": "mega", "age": 13})
663
+ db.set("users.2", {"name": "ratsa", "age": 14})
664
+ db.set("users.1.email", "mega@gmail.com")
665
+ """
666
+ {
667
+ "users": {
668
+ "1": {"name": "mega", "age": 13, "email": "mega@gmail.com"},
669
+ "2": {"name": "ratsa", "age": 14}
670
+ }
671
+ }
672
+ """
673
+
674
+ db.set("counter", {"id": "1239858345", "count": 48})
675
+
676
+ # con TTL: este documento se borra solo despues de 60 segundos
677
+ db.set("sesiones.s1", {"userId": "u1"}, {"ttl": 60000})
678
+ ```
679
+ ---
680
+
681
+ ## has
682
+ ### `has(path)`
683
+ Verifica si existe una clave en la colección de la ruta `path`.
684
+
685
+ * **`path` (str):** ruta a la clave a verificar, soporta dot notation `.`
686
+ * **Retorna:** `True` si existe, `False` si no.
687
+
688
+ ```python
689
+ db.set("users.1", {"name": "mega", "age": 13})
690
+ db.set("users.2", {"name": "ratsa", "age": 14})
691
+
692
+ print(db.has("users")) # True
693
+ print(db.has("users.1")) # True
694
+ print(db.has("users.1.name")) # True
695
+ print(db.has("users.2.age")) # True
696
+ print(db.has("users.1.status")) # False
697
+ print(db.has("users.1.name.data")) # False
698
+ print(db.has("docs")) # False
699
+ ```
700
+ ---
701
+
702
+ ## get
703
+ ### `get(path)`
704
+ Obtiene un documento almacenado por su clave.
705
+
706
+ * **`path` (str):** ruta al documento, soporta dot notation `.`
707
+ * **Retorna:** el valor almacenado o `None` si no existe.
708
+
709
+ ```python
710
+ db.set("users.1", {"name": "mega", "age": 13})
711
+ db.set("users.2", {"name": "ratsa", "age": 14})
712
+ db.set("docs", [])
713
+
714
+ user1 = db.get("users.1")
715
+ print(user1) # {'name': 'mega', 'age': 13}
716
+ age = db.get("users.2.age")
717
+ print(age) # 14
718
+ nodata = db.get("users.3")
719
+ print(nodata) # None
720
+ document = db.get("docs")
721
+ print(document) # []
722
+ ```
723
+ ---
724
+
725
+ ## delete
726
+ ### `delete(path)`
727
+ Elimina un documento almacenado por su clave.
728
+
729
+ * **`path` (str):** ruta del documento a eliminar, soporta dot notation `.`
730
+ * **Retorna:** `True` si se eliminó, `False` si no existía.
731
+
732
+ ```python
733
+ db.set("users.1", {"name": "mega", "age": 13})
734
+ db.set("users.2", {"name": "ratsa", "age": 14})
735
+ db.delete("users.2.age") # elimina el campo "age" del usuario 2
736
+ db.delete("users.1") # elimina todo el objeto del usuario 1
737
+
738
+ print(db.get("users.1")) # None
739
+ print(db.get("users")) # {'2': {'name': 'ratsa'}}
740
+ ```
741
+ ---
742
+
743
+ ## all
744
+ ### `all()`
745
+ Obtiene todos los documentos de la colección.
746
+
747
+ * **Retorna:** un dict con todos los documentos actuales.
748
+
749
+ ```python
750
+ db.set("users.1", {"name": "mega", "age": 13})
751
+ db.set("users.2", {"name": "ratsa", "age": 14})
752
+
753
+ print(db.all())
754
+ """
755
+ {
756
+ "users": {
757
+ "1": {"name": "mega", "age": 13},
758
+ "2": {"name": "ratsa", "age": 14}
759
+ }
760
+ }
761
+ """
762
+ ```
763
+
764
+ ## stream
765
+ ### `stream(path, query={})`
766
+ **Generador Python** cursor sobre `find()`. Para colecciones grandes: en vez de construir una lista completa en RAM (como `find()`/`all()`), recorre bloque por bloque y va entregando documentos con `yield` a medida que los encuentra, sin cargar la colección entera en memoria.
767
+
768
+ * **`path` (str):** igual que en `find`, ruta o `"*"`.
769
+ * **`query` (dict, opcional):** mismos operadores que `find`.
770
+ * **Retorna:** un generador de **valores** de documentos (no la clave, si necesitas la clave usa `entries()`).
771
+
772
+ ```python
773
+ for doc in db.stream("usuarios", {"edad": {"$gt": 18}}):
774
+ ... # procesa doc de a uno; nunca tiene toda la coleccion en RAM a la vez
775
+ ```
776
+ > Nota de memoria: si un bloque no estaba ya "tibio" en `blocksCache` antes de empezar el recorrido, se descarta de la caché al terminar de procesarlo. Si el bloque ya estaba cacheado de antes por otro uso, se respeta tal cual.
777
+
778
+ ## entries
779
+ ### `entries(path, query={})`
780
+ Igual que `stream()`, pero entrega `{"key": ..., "value": ...}` en vez de solo el valor. Hace falta cuando necesitas saber la clave del documento además de su contenido.
781
+
782
+ ```python
783
+ for entry in db.entries("*", {}):
784
+ print(entry["key"], entry["value"])
785
+ ```
786
+
787
+ ## keys
788
+ ### `keys(path)`
789
+ Devuelve todas las claves de la colección.
790
+
791
+ * **`path` (str):** ruta de las claves, soporta dot notation `.` y wildcard `*` para las claves principales.
792
+ * **Retorna:** una lista con las claves encontradas.
793
+
794
+ ```python
795
+ db.set("users.1", {"name": "mega", "age": 13})
796
+ db.set("users.2", {"name": "ratsa", "age": 14})
797
+ db.set("docs", [])
798
+
799
+ print(db.keys("users")) # ["1", "2"]
800
+ print(db.keys("users.1")) # ["name", "age"]
801
+ print(db.keys("*")) # wildcard => ["users", "docs"]
802
+ print(db.keys("notfound")) # []
803
+ print(db.keys("users.2.data")) # []
804
+ ```
805
+ ---
806
+
807
+ ## values
808
+ ### `values(path)`
809
+ Devuelve todos los valores de la colección.
810
+
811
+ * **`path` (str):** ruta de los valores, soporta dot notation `.` y wildcard `*`.
812
+ * **Retorna:** una lista con los valores encontrados.
813
+
814
+ ```python
815
+ db.set("users.1", {"name": "mega", "age": 13})
816
+ db.set("users.2", {"name": "ratsa", "age": 14})
817
+ db.set("docs", [])
818
+ db.set("status", False)
819
+
820
+ print(db.values("users")) # [{'name': 'mega', 'age': 13}, {'name': 'ratsa', 'age': 14}]
821
+ print(db.values("users.1")) # ['mega', 13]
822
+ print(db.values("users.1.name")) # []
823
+ print(db.values("users.3")) # []
824
+ print(db.values("*")) # wildcard => [{"1": {...}, "2": {...}}, [], False]
825
+ ```
826
+ ---
827
+
828
+ ## count
829
+ ### `count(path, query)`
830
+ Devuelve el número total de documentos.
831
+
832
+ * **`path` (str):** ruta donde se hará el conteo; `'*'` escanea toda la colección, soporta dot notation `.`
833
+ * **`query` (dict):** objeto de filtro, cada clave del dict es un campo o ruta de campo.
834
+ * **Retorna:** el conteo de elementos que pasó el filtro.
835
+
836
+ Filtro simple (igualdad):
837
+ ```python
838
+ {"edad": 30} # equivale a edad == 30
839
+ ```
840
+ Filtro avanzado con [operadores](#operadores-relacionales-y-textuales):
841
+ ```python
842
+ {"edad": {"$gt": 30}} # equivale a edad > 30
843
+ ```
844
+ ```python
845
+ db.set("users.1", {"name": "mega", "edad": 13})
846
+ db.set("users.2", {"name": "megast", "edad": 14})
847
+ db.set("users.3", {"name": "pedro", "edad": 15})
848
+ db.set("users.4", {"name": "ratsa", "edad": 16})
849
+
850
+ print(db.count("users", {"edad": {"$gt": 14}})) # 2
851
+ print(db.count("users", {"edad": {"$lt": 16}})) # 3
852
+ print(db.count("users", {"edad": {"$gt": 14}, "name": {"$prefix": "r"}})) # 1
853
+ print(db.count("*", {})) # wildcard, toda la coleccion
854
+ print(db.count("data.dato2", {})) # dot notation
855
+ ```
856
+ ---
857
+
858
+ ## find
859
+ ### `find(path, query={})`
860
+ Busca y devuelve una lista con los documentos (o sub-documentos) que cumplen el filtro `query` dentro de la ruta indicada por `path`. Aprovecha índices secundarios (`secondaryIndexes`) cuando están configurados.
861
+
862
+ * **`path` (str, opcional):** ruta del documento donde se aplicará la búsqueda; `'*'` escanea toda la colección, soporta dot notation `.`
863
+ * **`query` (dict, opcional):** objeto de filtro.
864
+ * **Retorna:** una lista con los documentos que cumplieron el filtro.
865
+
866
+ *Soporta 2 formas de llamada:*
867
+ ```python
868
+ modo_1 = db.find(path, query)
869
+ modo_2 = db.find(query) # path se asume '*'
870
+ ```
871
+
872
+ Filtro simple:
873
+ ```python
874
+ {"edad": 30} # equivale a edad == 30
875
+ ```
876
+ Filtro avanzado con operadores:
877
+ ```python
878
+ {"edad": {"$gt": 30}} # equivale a edad > 30
879
+ ```
880
+
881
+ * Ejemplos (con explicaciones)
882
+
883
+ ```python
884
+ db.set('usuario1', {"nombre": "u1", "edad": 20})
885
+ db.set('usuario2', {"nombre": "u2", "edad": 19})
886
+ db.set('usuario3', {"nombre": "u3", "edad": 18})
887
+
888
+ print(db.find({"nombre": "u1"})) # [{'nombre': 'u1', 'edad': 20}]
889
+ print(db.find({"edad": {"$eq": 20}})) # [{'nombre': 'u1', 'edad': 20}]
890
+ print(db.find({"edad": {"$gt": 19}})) # [{'nombre': 'u1', 'edad': 20}]
891
+ print(db.find({"edad": {"$lt": 19}})) # [{'nombre': 'u3', 'edad': 18}]
892
+ print(db.find({"nombre": {"$in": ["u1", "u3"]}}))
893
+ # [{'nombre': 'u1', 'edad': 20}, {'nombre': 'u3', 'edad': 18}]
894
+ print(db.find({"nombre": {"$nin": ["u1", "u3"]}}))
895
+ # [{'nombre': 'u2', 'edad': 19}]
896
+ print(db.find({"nombre": {"$prefix": "u"}}))
897
+ # los 3 documentos
898
+ print(db.find({"nombre": {"$regex": "^u", "$flags": "i"}}))
899
+ # los 3 documentos
900
+ ```
901
+ * También con dot notation:
902
+ ```python
903
+ db.set("usuarios.pablo", {
904
+ "id": "u1", "nombre": "pablo pablon", "edad": 42, "rol": "admin",
905
+ "direccion": {"ciudad": "xxxx", "distrito": "aaaaaa"}, "bio": "texto largo...",
906
+ })
907
+ db.set("usuarios.pedro", {
908
+ "id": "u2", "nombre": "pedro pedron", "edad": 17, "rol": "moderador",
909
+ "direccion": {"ciudad": "yyyyy", "distrito": "eeeeee"}, "bio": "texto largo...",
910
+ })
911
+ db.set("usuarios.juan", {
912
+ "id": "u3", "nombre": "juan juanon", "edad": 25, "rol": "trusted",
913
+ "direccion": {"ciudad": "zzzzz", "distrito": "iiiiii"}, "bio": "texto largo...",
914
+ })
915
+
916
+ print(db.find('usuarios', {'direccion.ciudad': 'yyyyy'})) # documento de pedro
917
+ print(db.find('usuarios', {'nombre': {'$prefix': 'ju'}})) # documento de juan
918
+ print(db.find('usuarios', {'edad': {'$between': [20, 40]}})) # documento de juan
919
+ print(db.find('usuarios', {'rol': {'$in': ['admin', 'moderador', 'editor']}}))
920
+ # documentos de pedro y pablo
921
+
922
+ # Tambien puedes usar dot notation en el path:
923
+ db.find("dato1.dato2", {"propiedad1": {"$nin": ["valores", "a", "verificar"]}})
924
+ db.find("dato1.dato2", {"data4.data5": "valor"})
925
+ ```
926
+ * En miles de datos, esto se agiliza si usas `secondaryIndexes`.
927
+ ---
928
+
929
+ ## aggregate
930
+ ### `aggregate(path, pipeline)`
931
+ Ejecuta operaciones de agregación sobre los documentos en la ruta indicada por `path`. Permite un pipeline de etapas (`$match`, `$group`, `$sort`, `$limit`) para transformaciones y cálculos analíticos.
932
+
933
+ * **`path` (str):** ruta; `'*'` escanea toda la colección, soporta dot notation `.`
934
+ * **`pipeline` (list):** lista de etapas, cada elemento un dict con una sola clave (ej: `{"$match": {...}}`).
935
+ * **Retorna:** una lista con los resultados de la agregación.
936
+
937
+ * Etapas soportadas:
938
+ * `$match` => Filtra documentos (igual que [find](#find)).
939
+ * `$group` => Agrupa por un campo (`_id`) y permite acumuladores (`$sum`, `$avg`, `$min`, `$max`, `$push`).
940
+ - `$` => Para usar un campo específico en un acumulador, colocá `$` antes del nombre: `"$rol"`.
941
+ - `_id`: obligatorio para agrupar por campo, ej: `"$rol"`.
942
+ - `$sum` => suma valores numéricos. Ej: `"totalEdad": {"$sum": "$edad"}`, `"total": {"$sum": 1}`.
943
+ - `$avg` => promedio de un campo numérico. Ej: `"promedio": {"$avg": "$edad"}`.
944
+ - `$min` / `$max` => valor mínimo/máximo por grupo.
945
+ * `$sort` => Ordena resultados (`-1` descendente, `1` ascendente), soporta multi-campo.
946
+ * `$limit` => Limita la cantidad de resultados finales.
947
+
948
+ #### Ejemplos, agregando primero estos datos:
949
+ ```python
950
+ db.set("usuarios.pablo", {"id": "u1", "nombre": "pablo pablon", "edad": 42, "rol": "admin"})
951
+ db.set("usuarios.pedro", {"id": "u2", "nombre": "pedro pedron", "edad": 17, "rol": "moderador"})
952
+ db.set("usuarios.juan", {"id": "u3", "nombre": "juan juanon", "edad": 25, "rol": "trusted"})
953
+ db.set("usuarios.patricio", {"id": "u4", "nombre": "patricio patrico", "edad": 26, "rol": "trusted"})
954
+ ```
955
+
956
+ ##### Filtrar con $match y agrupar con $group
957
+ ```python
958
+ data = db.aggregate("usuarios", [
959
+ {"$match": {"id": {"$prefix": "u"}}},
960
+ {"$group": {"_id": "$rol", "total": {"$sum": 1}}},
961
+ ])
962
+ print(data)
963
+ # [{'_id': 'trusted', 'total': 2}, {'_id': 'admin', 'total': 1}, {'_id': 'moderador', 'total': 1}]
964
+ ```
965
+ ##### Acumuladores $sum, $avg, $min, $max
966
+ ```python
967
+ data = db.aggregate("usuarios", [
968
+ {"$group": {
969
+ "_id": "$rol",
970
+ "total": {"$sum": 1},
971
+ "edadPromedio": {"$avg": "$edad"},
972
+ "edadMinima": {"$min": "$edad"},
973
+ "edadMaxima": {"$max": "$edad"},
974
+ }},
975
+ ])
976
+ ```
977
+ ##### Ordenar con $sort
978
+ ```python
979
+ data = db.aggregate("usuarios", [
980
+ {"$group": {"_id": "$rol", "total": {"$sum": 1}, "edadPromedio": {"$avg": "$edad"}}},
981
+ {"$sort": {"total": -1, "edadPromedio": 1}},
982
+ ])
983
+ ```
984
+ ##### Limitar con $limit
985
+ ```python
986
+ data = db.aggregate("usuarios", [
987
+ {"$group": {"_id": "$rol", "total": {"$sum": 1}}},
988
+ {"$sort": {"total": -1}},
989
+ {"$limit": 1},
990
+ ])
991
+ ```
992
+ Para toda la colección, usa `db.aggregate("*", [...])`.
993
+
994
+ ## update
995
+ ### `update(path, ops)`
996
+ Actualiza uno o varios valores dentro de un documento en la ruta indicada por `path`. `ops` define las operaciones a ejecutar; puedes combinar más de una.
997
+
998
+ * **`path` (str):** ruta del documento, soporta dot notation `.`
999
+ * **`ops` (dict):** una o más operaciones de actualización.
1000
+ * **Retorna:** el documento actualizado.
1001
+
1002
+ * Operaciones soportadas:
1003
+ - `$set` => establece o crea una propiedad. Ej: `{"$set": {"nombre": "nuevo nombre"}}`.
1004
+ - `$inc` => incrementa/decrementa un número. Ej: `{"$inc": {"edad": 1}}`.
1005
+ - `$unset` => elimina una propiedad. Ej: `{"$unset": {"edad": ""}}`.
1006
+ - `$push` => agrega elementos a un array (lista de valores a agregar). Ej: `{"$push": {"frutas": ["manzana", "durazno"]}}`.
1007
+ - `$pull` => elimina elementos de un array si existen. Ej: `{"$pull": {"frutas": ["manzana"]}}`.
1008
+
1009
+ #### Ejemplos
1010
+ ```python
1011
+ db.set("mario", {"nick": "mario30000", "edad": 13, "frutas": []})
1012
+ db.set("pedro", {"nick": "pedro20000", "edad": 11, "frutas": []})
1013
+ db.set("juan", {"nick": "juan10000", "edad": 15, "frutas": []})
1014
+ ```
1015
+ #### Cambiar nick y sumar edad
1016
+ ```python
1017
+ nuevos_datos = db.update("mario", {"$set": {"nick": "marioPRO30000"}, "$inc": {"edad": 1}})
1018
+ print(nuevos_datos) # {'nick': 'marioPRO30000', 'edad': 14, 'frutas': []}
1019
+ ```
1020
+ #### Eliminar campo frutas y añadir verduras
1021
+ ```python
1022
+ nuevos_datos = db.update("pedro", {"$unset": {"frutas": ""}, "$set": {"verduras": []}})
1023
+ ```
1024
+ #### Agregar frutas y luego eliminar una
1025
+ ```python
1026
+ db.update("juan", {"$push": {"frutas": ["manzana", "durazno"]}})
1027
+ db.update("juan", {"$pull": {"frutas": ["durazno"]}})
1028
+ ```
1029
+ #### Varias operaciones a la vez
1030
+ ```python
1031
+ db.update("juan", {
1032
+ "$set": {"juegos.overwatch": "TAG#AAAA", "amigos": []},
1033
+ "$push": {"frutas": ["platano"]},
1034
+ "$inc": {"edad": 1},
1035
+ })
1036
+ # con dot notation => update("path1.path2.path3.etc")
1037
+ ```
1038
+
1039
+ ## watch
1040
+ ### `watch(path, callback)`
1041
+ Permite observar cambios en tiempo real sobre un documento o ruta específica.
1042
+ Cada vez que ocurre una operación de escritura en esa ruta, se dispara el callback.
1043
+
1044
+ * **`path` (str):** ruta a observar, documento exacto, colección con wildcard (`"usuarios.*"` o `"usuarios"`), o `'*'` para toda la colección.
1045
+ * **`callback` (función):** función que se ejecuta con 4 parámetros:
1046
+ * `path (str)` => ruta completa del valor afectado.
1047
+ * `new_val (any)` => valor nuevo.
1048
+ * `old_val (any)` => valor anterior.
1049
+ * `op (str)` => `"set"`, `"delete"` o `"update"`.
1050
+
1051
+ ```python
1052
+ db.set("mario", {"nick": "mario30000", "edad": 13, "frutas": []})
1053
+ db.set("pedro", {"nick": "pedro20000", "edad": 11, "frutas": []})
1054
+ db.set("juan", {"nick": "juan10000", "edad": 15, "frutas": []})
1055
+ ```
1056
+ #### Observar un documento específico
1057
+ ```python
1058
+ def on_mario_change(path, new_val, old_val, op):
1059
+ print(f"[WATCH] Ruta: {path}")
1060
+ print(f"Operación: {op}")
1061
+ print("Antes:", old_val)
1062
+ print("Ahora:", new_val)
1063
+
1064
+ db.watch("mario", on_mario_change)
1065
+ db.set("mario", {"nick": "mario30000", "edad": 14, "frutas": []})
1066
+ ```
1067
+ #### Observar toda la colección
1068
+ ```python
1069
+ db.watch("*", lambda path, new_val, old_val, op: print(f"[WATCH] Cambio en {path} ({op})"))
1070
+ db.set("juan", {"nick": "juan10000", "edad": 16, "frutas": []})
1071
+ ```
1072
+ #### Detectar eliminación
1073
+ ```python
1074
+ def on_pedro_change(path, new_val, old_val, op):
1075
+ if op == "delete":
1076
+ print(f"El usuario en {path} fue eliminado.", old_val)
1077
+
1078
+ db.watch("pedro", on_pedro_change)
1079
+ db.delete("pedro")
1080
+ ```
1081
+ #### detectar cambios en arrays
1082
+ ```python
1083
+ def on_mario_update(path, new_val, old_val, op):
1084
+ if op == "update":
1085
+ print(f"[WATCH] Se actualizó la fruta de {path}")
1086
+ print("Antes:", old_val["frutas"])
1087
+ print("Ahora:", new_val["frutas"])
1088
+
1089
+ db.watch("mario", on_mario_update)
1090
+ db.update("mario", {"$push": {"frutas": ["manzana"]}})
1091
+
1092
+ # Tambien puedes usar dot notation: db.watch("datos.usuario1", ...)
1093
+ ```
1094
+
1095
+ ## stats
1096
+ ### `stats()`
1097
+ Devuelve un dict con estadísticas internas de la base de datos, orientado a monitoreo/depuración.
1098
+ * **Retorna:**
1099
+ * `keys` => cantidad total de claves registradas en el índice principal.
1100
+ * `dirtyBlocks` => número de bloques "sucios" (modificados en memoria, aún no persistidos).
1101
+ * `cachedBlocks` => número de bloques actualmente en caché.
1102
+ * `secondaryIndexes` => cantidad de índices secundarios definidos.
1103
+ * `keyCache` => `{"size": ..., "capacity": ...}`.
1104
+ ```python
1105
+ # ejemplo db.stats()
1106
+ {
1107
+ "keys": 3,
1108
+ "dirtyBlocks": 1,
1109
+ "cachedBlocks": 1,
1110
+ "secondaryIndexes": 2,
1111
+ "keyCache": {"size": 3, "capacity": 1000},
1112
+ }
1113
+ ```
1114
+
1115
+ ## flush
1116
+ ### `flush()`
1117
+ Fuerza la escritura inmediata en disco de todos los datos en memoria (que aún no se persistieron por el `flushInterval` automático).
1118
+
1119
+ ```python
1120
+ db.set(...)
1121
+ db.flush()
1122
+ ```
1123
+ ## close
1124
+ ### `close()`
1125
+ Cierra la base de datos de manera ordenada: hace flush de todo lo pendiente y detiene los timers/hilos internos. Recomendado al final del ciclo de vida de tu proyecto.
1126
+ ```python
1127
+ db.close()
1128
+ ```
1129
+
1130
+ ## createBackup
1131
+ ### `createBackup()`
1132
+ Crea un respaldo persistente del estado actual de la base de datos, con un nombre único (`counter` o `timestamp` según `backupMode`). Junto al archivo `.json.backup` se guarda también un `.sha256` con el checksum del contenido.
1133
+
1134
+ * **Retorna:** `True` si se creó, `False` si no.
1135
+ ```python
1136
+ db.createBackup()
1137
+ ```
1138
+
1139
+ ## restoreBackup
1140
+ ### `restoreBackup(name, opts={})`
1141
+ Restaura un backup previamente creado, sobrescribiendo el estado actual de la base de datos.
1142
+
1143
+ **El checksum del backup se verifica ANTES de tocar la base de datos.** Si está corrupto, lanza un `MegaDBError` explícito y no aplica nada:
1144
+ ```python
1145
+ from megadbx.classes.mega_db_error import MegaDBError
1146
+ try:
1147
+ db.restoreBackup("backup_00000")
1148
+ except MegaDBError as err:
1149
+ print(err) # 'El backup "backup_00000" esta corrupto ...'
1150
+ ```
1151
+
1152
+ * **`name` (str):** nombre del backup a restaurar.
1153
+ * **`opts` (dict, opcional):**
1154
+ - `requireChecksum` (bool, default `False`): si `True`, rechaza también backups viejos sin `.sha256`.
1155
+ * **Retorna:** `True` si se restauró, `False` si no.
1156
+ ```python
1157
+ db.restoreBackup("backup-2025-09-05T18-00-00")
1158
+ ```
1159
+
1160
+ ## listBackups
1161
+ ### `listBackups()`
1162
+ Lista los backups disponibles para esta colección.
1163
+
1164
+ * **Retorna:** una lista de `{"name": ..., "sizeBytes": ..., "createdAt": ...}`, ordenada del mas reciente al mas viejo.
1165
+ ```python
1166
+ backups = db.listBackups()
1167
+ # [{'name': 'backup_00003', 'sizeBytes': 4820, 'createdAt': 1735500000000.0}, ...]
1168
+ ```
1169
+
1170
+ ## createSnapshot
1171
+ ### `createSnapshot()`
1172
+ Genera un snapshot en memoria (bytes) del estado actual de la base de datos. Útil para enviar por red, guardar temporalmente o clonar estados.
1173
+ * **Retorna:** `bytes` con el snapshot serializado.
1174
+ ```python
1175
+ snapshot = db.createSnapshot()
1176
+ ```
1177
+
1178
+ ## restoreSnapshot
1179
+ ### `restoreSnapshot(buffer)`
1180
+ Restaura un snapshot previamente creado con `createSnapshot()`, sobrescribe completamente el estado actual.
1181
+
1182
+ * **`buffer` (bytes)**: snapshot previamente guardado.
1183
+ * **Retorna:** `True` si se realizó, `False` si no.
1184
+ ```python
1185
+ snap = db.createSnapshot()
1186
+ # ... guardas miles de datos, luego decides volver atrás ...
1187
+ estado = db.restoreSnapshot(snap)
1188
+ print(estado) # True
1189
+ ```
1190
+ ---
1191
+ # 2. `MegaDBSafe`
1192
+
1193
+ ## MegaDBSafe hereda de MegaDB, y añade capacidades pensadas para concurrencia, durabilidad y multi-proceso:
1194
+ * **WAL (Write-Ahead Log)**
1195
+ * Todas las escrituras (`set`, `delete`, `update`) primero se registran en un log (`.wal`).
1196
+ * Si la app se cae, al reiniciar se hace replay de ese log para restaurar la consistencia.
1197
+ * **IndexStore**
1198
+ * Persiste índices secundarios (`secondaryIndexes`) en un archivo separado y los recarga rápido al iniciar.
1199
+ * **OpQueue (QueuedLock)**
1200
+ * Serializa operaciones (basado en `threading.RLock`) dentro de un mismo proceso.
1201
+ * Garantiza que múltiples llamados de escritura/guardado concurrentes (por ejemplo, desde varios hilos) no choquen.
1202
+ * **Stats extendido.**
1203
+ * **Pensado para proyectos medianos/grandes.**
1204
+
1205
+ ## Conceptos clave más importantes
1206
+ * **WAL (Write-Ahead Log)**: log secuencial en disco donde se escriben todas las operaciones antes de aplicarlas a los bloques. Garantiza durabilidad.
1207
+ * **OpQueue (QueuedLock)**: serializa operaciones concurrentes dentro del mismo proceso; evita que dos escrituras se pisen.
1208
+ * **IndexStore**: persiste y restaura los índices secundarios en un archivo separado.
1209
+
1210
+ ## MegaDBSafe
1211
+ ```python
1212
+ from megadbx import MegaDBSafe
1213
+ db = MegaDBSafe(collection_name, options={})
1214
+ ```
1215
+
1216
+ ## MegaDBSafe contiene todo lo que MegaDB ya tiene, metodos y opciones, más:
1217
+
1218
+ | Opción | Default | Explicación |
1219
+ | ----------- | ------- | ----------------------------------------------------- |
1220
+ | `replayWal` | `True` | Si al iniciar encuentra un archivo `.wal`, lo reproduce. |
1221
+
1222
+ `replayWal`: Si al iniciar encuentra un archivo `.wal`, lo reproduce para aplicar operaciones pendientes y restaurar consistencia.
1223
+ - `True` → siempre reejecuta las operaciones pendientes.
1224
+ - `False` → ignora el WAL (solo recomendado para debugging).
1225
+ - El WAL no es acumulativo: se reinicia entre cada arranque.
1226
+
1227
+ Puedes seguir usando todas las opciones del constructor de [MegaDB](#megadb).
1228
+
1229
+ ## ready
1230
+ ### `ready()`
1231
+ En la versión síncrona, la inicialización ya termino cuando el constructor retorna, `ready()` existe por compatibilidad y simplemente retorna `True`. No hace falta llamarlo, pero puedes hacerlo si tu codigo viene de un patron que lo esperaba.
1232
+ ```python
1233
+ db = MegaDBSafe("data", {"dir": "./", "secondaryIndexes": ["rol"], "replayWal": True})
1234
+ db.ready() # True, opcional
1235
+
1236
+ # resto de la logica de tu proyecto...
1237
+ ```
1238
+ ## set / has / get / delete / all / stream / entries / keys / values / count / find / aggregate / update / watch
1239
+ Es lo mismo que `MegaDB.<metodo>`, ver la sección de [MegaDB](#megadb).
1240
+
1241
+ ## stats
1242
+ ### `stats()`
1243
+ Es lo mismo que `MegaDB.stats()` pero contiene más cosas:
1244
+ * `wal` (dict): estado del Write-Ahead Log, `enabled`, `file`, `pendingOps`.
1245
+ * `locks` (dict): `queueLength` operaciones esperando turno en el OpQueue.
1246
+ * `indexStore` (dict): `loaded`, `file`.
1247
+ ```python
1248
+ # ejemplo: db.stats()
1249
+ {
1250
+ "keys": 120,
1251
+ "dirtyBlocks": 2,
1252
+ "cachedBlocks": 8,
1253
+ "secondaryIndexes": 2,
1254
+ "keyCache": {"size": 80, "capacity": 1000},
1255
+ "wal": {"enabled": True, "pendingOps": 3, "file": "./db/data/wal.log"},
1256
+ "locks": {"queueLength": 2},
1257
+ "indexStore": {"loaded": True, "file": "./db/data/indexes.json"},
1258
+ }
1259
+ ```
1260
+
1261
+ ## flush / close / createBackup / restoreBackup / listBackups / createSnapshot / restoreSnapshot
1262
+ Es lo mismo que `MegaDB.<metodo>`, ver la sección de [MegaDB](#megadb).
1263
+
1264
+ ---
1265
+ # 3. `MegaDBFull`
1266
+
1267
+ ## MegaDBFull hereda de MegaDBSafe, y añade funcionalidades avanzadas:
1268
+ * **Índices avanzados**
1269
+ * CompositeIndexes => índices sobre combinaciones de campos.
1270
+ * TrieIndexes => índices de prefijos para $prefix.
1271
+ * SkipListIndexes => índices ordenados para $between/$gt/etc.
1272
+ * **MVCC (Multi-Version Concurrency Control)**
1273
+ * Guarda múltiples versiones de un mismo documento.
1274
+ * Soporta queries por versión (`$version`) o por timestamp (`$since`).
1275
+ * Método nuevo: `history()`.
1276
+ * **Append-Only Log (AOF)**
1277
+ * Todas las operaciones de escritura se registran en un archivo append-only (`aof.log`).
1278
+ * Opción de replay (`replayLog`).
1279
+ * **Merkle Tree**
1280
+ * Verifica integridad (`verifyIntegrityFast`) y detecta corrupción de datos.
1281
+ * **Stats más detallado.**
1282
+ * **Consultas optimizadas (`find`)**: usa composite, trie y skiplist para reducir el escaneo.
1283
+
1284
+ ## Conceptos clave más importantes
1285
+ * `CompositeIndexes` => combinan varios campos en un índice, acelerando consultas multidimensionales.
1286
+ * `TrieIndexes` => perfectos para búsquedas de prefijo con `$prefix`.
1287
+ * `SkipListIndexes` => consultas por rangos numéricos muy rápido.
1288
+ * `MVCC` => consistencia en concurrencia + consulta de versiones históricas.
1289
+ * `Append-Only Log` => cada operación se escribe secuencialmente; útil para recuperación y auditoría.
1290
+ * `Merkle Tree` => garantiza integridad de datos, detectando corrupción o cambios no autorizados.
1291
+
1292
+ ## MegaDBFull
1293
+ ```python
1294
+ from megadbx import MegaDBFull
1295
+ db = MegaDBFull(collection_name, options={})
1296
+ ```
1297
+
1298
+ ## MegaDBFull contiene todo lo que MegaDB y MegaDBSafe ya tienen, más:
1299
+
1300
+ | Opción | Default | Explicación |
1301
+ | ------------------------- | -------------- | -------------------------------------------------------------------------------- |
1302
+ | `compositeIndexes` | `[]` | Lista de definiciones de índices compuestos (campos múltiples). |
1303
+ | `compositePaged` | `False` | Guarda el índice compuesto paginado en disco (por hash-shard) en vez de un solo archivo. |
1304
+ | `compositeNumShards` | `32` | Cantidad de shards (archivos) por spec, en modo `compositePaged`. |
1305
+ | `compositeShardCacheCap` | `16` | Shards simultáneos en RAM (LRU) por spec. |
1306
+ | `trieIndexes` | `[]` | Campos indexados con trie, optimizados para `$prefix`. |
1307
+ | `triePaged` | `False` | Guarda el índice trie paginado en disco (por prefijo). |
1308
+ | `trieShardDepth` | `2` | Cuántos caracteres del string determinan el shard, en modo `triePaged`. |
1309
+ | `trieShardCacheCap` | `16` | Shards simultáneos en RAM (LRU) por campo. |
1310
+ | `skiplistIndexes` | `[]` | Campos indexados con skiplist, optimizados para `$between`, `$gt`, etc. |
1311
+ | `skiplistPaged` | `False` | Guarda el índice skiplist paginado en disco. |
1312
+ | `skiplistPageSize` | `500` | Entradas por página, en modo `skiplistPaged`. |
1313
+ | `skiplistPageCacheCap` | `32` | Páginas simultáneas en RAM (LRU) por campo. |
1314
+ | `mvcc` | `{}` | Objeto con las opciones mvcc. |
1315
+ | `mvcc.enabled` | `False` | Activa el control multiversión. |
1316
+ | `mvcc.retainVersions` | `3` | Número de versiones que se retienen por clave. |
1317
+ | `appendOnly` | `{}` | Objeto con las opciones append-only (aof). |
1318
+ | `appendOnly.enabled` | `False` | Activa el log append-only. |
1319
+ | `appendOnly.file` | `aof.log` | Nombre del archivo donde se guarda el log. |
1320
+ | `appendOnly.fsync` | `False` | Forzar escritura en disco inmediatamente. |
1321
+ | `appendOnly.replayLog` | `False` | Si `True`, al iniciar se hace replay del log para recuperar datos. |
1322
+ | `merkle` | `{}` | Objeto con las opciones del merkle. |
1323
+ | `merkle.enabled` | `False` | Activa el árbol de Merkle para verificar integridad. |
1324
+ | `merkle.file` | `merkle.json` | Archivo donde se guarda el árbol. |
1325
+
1326
+ ## Índices paginados en disco (CompositeIndexManager, TrieIndexManager y SkipListIndexManager)
1327
+
1328
+ Por default, los tres índices viven **enteros en RAM** y se persisten como un solo archivo JSON. Con `*Paged: True`, cada uno reparte sus datos en varios archivos chicos en disco, cargando en RAM solo lo que una consulta puntual necesita:
1329
+
1330
+ - **`CompositeIndexManager`** (`compositePaged`) particionado por **hash** en `compositeNumShards` archivos fijos.
1331
+ - **`TrieIndexManager`** (`triePaged`) particionado por **prefijo literal** (los primeros `trieShardDepth` caracteres).
1332
+ - **`SkipListIndexManager`** (`skiplistPaged`) particionado en **páginas** de `skiplistPageSize` entradas ordenadas, con un directorio chico en RAM.
1333
+
1334
+ Los tres usan una caché LRU acotada para no volver a "cargar todo en RAM" por la puerta trasera.
1335
+
1336
+ ```python
1337
+ db = MegaDBFull("productos", {
1338
+ "compositeIndexes": [{"name": "cat_idx", "fields": ["categoria"]}],
1339
+ "compositePaged": True, "compositeNumShards": 32,
1340
+
1341
+ "trieIndexes": ["nombre"],
1342
+ "triePaged": True, "trieShardDepth": 2,
1343
+
1344
+ "skiplistIndexes": ["precio"],
1345
+ "skiplistPaged": True, "skiplistPageSize": 500,
1346
+ })
1347
+ ```
1348
+
1349
+ **¿Cuándo usarlos?** Si tu colección tiene miles de documentos y tus índices ya no caben comodos en memoria. Para colecciones chicas/medianas, el modo default (todo en RAM) sigue siendo más simple y más rápido.
1350
+
1351
+ ## Qué es un CompositeIndex?
1352
+ Un índice compuesto combina varios campos de un documento en una sola clave interna, acelerando consultas que usan esos campos juntos.
1353
+
1354
+ ```python
1355
+ db = MegaDBFull("usuarios", {
1356
+ "compositeIndexes": [{"name": "001", "fields": ["data", "pais"]}]
1357
+ })
1358
+ ```
1359
+ * **name** => identificador unico del índice.
1360
+ * **fields** => lista de campos a combinar.
1361
+
1362
+ #### Internamente MegaDBFull compone esto asi:
1363
+ * Para cada documento, toma los valores de los campos en orden.
1364
+ * Los concatena con el separador `|^|` (ej: `{"data": "A", "pais": "USA"}` = `"A|^|USA"`).
1365
+ * Si un campo falta, es `None`, o es un objeto/lista => no se indexa.
1366
+ #### Persistencia
1367
+ * Se guardan en **indexes_composite.json**.
1368
+ #### Ejemplo
1369
+ ```python
1370
+ db = MegaDBFull("datos", {"compositeIndexes": [{"name": "001", "fields": ["data", "pais"]}]})
1371
+
1372
+ db.set("usuarios.u1", {"id": "u1", "data": "A", "pais": "USA"})
1373
+ db.set("usuarios.u2", {"id": "u2", "data": "B", "pais": "USA"})
1374
+ db.set("usuarios.u3", {"id": "u3", "data": "A", "pais": "MX"})
1375
+
1376
+ data = db.find("usuarios", {"data": "A", "pais": "USA"})
1377
+ print(data) # [{'id': 'u1', 'data': 'A', 'pais': 'USA'}]
1378
+ ```
1379
+ * Acelera queries que combinan varios campos a la vez con igualdad exacta, evitando escaneos completos.
1380
+ * Solo trabaja con los datos agregados despues de activarlo. Para sincronizar datos preexistentes, usa [`rebuildAllIndexes`](#rebuildAllIndexes).
1381
+ * Soporta dot notation en `fields`.
1382
+
1383
+ ## Qué es un TrieIndex?
1384
+ Un Trie (árbol de prefijos) es una estructura optimizada para búsquedas por prefijo de texto.
1385
+
1386
+ ```python
1387
+ db = MegaDBFull("datos", {"trieIndexes": ["nick", "direccion.ciudad"]})
1388
+ ```
1389
+ #### Inserción interna
1390
+ ```python
1391
+ db.set("usuarios.mario", {"nick": "mario"})
1392
+ db.set("usuarios.marco", {"nick": "marco"})
1393
+ ```
1394
+ El trie comparte el camino `"m" -> "a" -> "r"` entre ambos, y luego se ramifica en `"i"->"o"` (mario) y `"c"->"o"` (marco).
1395
+ #### Consulta por prefijo ($prefix)
1396
+ ```python
1397
+ db.find("usuarios", {"nick": {"$prefix": "mar"}})
1398
+ # [{'nick': 'mario'}, {'nick': 'marco'}]
1399
+ ```
1400
+ #### Persistencia
1401
+ * Se guardan en **trie_indexes.json**.
1402
+
1403
+ `trieIndexes` acelera búsquedas por prefijo `$prefix`, pasando de escanear toda la colección a recorrer solo la rama del prefijo. Solo trabaja con datos agregados después de activarlo. Soporta dot notation.
1404
+
1405
+ ## Qué es un skiplistIndex?
1406
+ Un skiplist es una estructura para búsquedas/inserciones/eliminaciones rápidas en listas ordenadas, rendimiento cercano a O(log n).
1407
+
1408
+ #### Para qué sirve en MegaDBFull?
1409
+ Acelera consultas con `$gt`, `$lt`, `$gte`, `$lte`, `$between` sobre campos numéricos.
1410
+
1411
+ ```python
1412
+ db = MegaDBFull("datos", {"skiplistIndexes": ["edad", "score"]})
1413
+
1414
+ db.set("usuarios.pablo", {"nombre": "pablo", "edad": 42})
1415
+ db.set("usuarios.pedro", {"nombre": "pedro", "edad": 17})
1416
+ db.set("usuarios.juan", {"nombre": "juan", "edad": 25})
1417
+ db.set("usuarios.maria", {"nombre": "maria", "edad": 30})
1418
+
1419
+ print(db.find("usuarios", {"edad": {"$gt": 20}}))
1420
+ print(db.find("usuarios", {"edad": {"$between": [18, 30]}}))
1421
+ ```
1422
+ #### Internamente
1423
+ ```python
1424
+ state = {"edad": [{"v": 17, "k": "usuarios.pedro"}, {"v": 25, "k": "usuarios.juan"}, ...]}
1425
+ ```
1426
+ Cada campo indexado mantiene una lista de `{v, k}` ordenada por `v` (inserción/eliminación por búsqueda binaria).
1427
+ #### Persistencia
1428
+ * Se guardan en **skip_indexes.json**.
1429
+
1430
+ `skiplistIndexes` solo trabaja con datos agregados después de activarlo. Soporta dot notation.
1431
+ ---
1432
+ ## Qué es MVCC? (Multi-Version Concurrency Control)
1433
+ Mantiene múltiples versiones históricas de un documento, útil para auditoría, históricos de cambios o lecturas consistentes en paralelo.
1434
+
1435
+ - `mvcc` es un dict con las opciones:
1436
+ - `mvcc.enabled`: activa/desactiva MVCC (default: `False`).
1437
+ - `mvcc.retainVersions`: cuántas versiones mantener por documento (default: 3).
1438
+ ```python
1439
+ db = MegaDBFull("data", {"mvcc": {"enabled": True, "retainVersions": 2}})
1440
+ ```
1441
+ * MVCC solo trabaja con las rutas exactas que pasan por `MegaDBFull.set()`.
1442
+ * Se usa en `get`, `has`, `all`, `keys`, `values`, `aggregate`, `count`, `history`.
1443
+
1444
+ ## Qué es Append-Only (AOF)
1445
+ Modo de persistencia donde cada cambio se agrega al final de un archivo de log, un "diario" de operaciones. A diferencia del WAL, aquí se almacena todo como historial, nunca se borra nada. Sirve incluso para exportar/replicar datos de una instancia a otra solo usando el `.log`.
1446
+
1447
+ ```python
1448
+ db = MegaDBFull("data", {
1449
+ "appendOnly": {
1450
+ "enabled": True,
1451
+ "file": "aof.log",
1452
+ "fsync": True,
1453
+ "replayLog": True,
1454
+ }
1455
+ })
1456
+ ```
1457
+
1458
+ ## Qué es Merkle?
1459
+ Un árbol de Merkle es una estructura hash que permite verificar la integridad de los datos. Si un dato cambia, el hash de la raíz cambia si alguien manipula un archivo, el hash ya no coincide y la DB detecta la corrupción.
1460
+
1461
+ ```python
1462
+ db = MegaDBFull("datos", {"merkle": {"enabled": True, "file": "merkle.json"}})
1463
+ ```
1464
+ ---
1465
+ Puedes seguir usando las opciones del constructor de [MegaDB](#megadb) y [MegaDBSafe](#megadbsafe).
1466
+
1467
+ ## ready
1468
+ ### `ready()`
1469
+ Igual que en [MegaDBSafe](#ready): en la versión síncrona, retorna `True`, la inicialización ya terminó dentro del constructor.
1470
+ ```python
1471
+ db = MegaDBFull("data", {
1472
+ "dir": "./",
1473
+ "secondaryIndexes": ["rol"],
1474
+ "compositeIndexes": [{"name": "001", "fields": ["data", "pais"]}],
1475
+ "replayWal": True,
1476
+ "merkle": {"enabled": True},
1477
+ })
1478
+ db.ready() # True, opcional
1479
+ ```
1480
+ ## set
1481
+ ### `set(path, value)`
1482
+ Igual que `MegaDB.set(path, value)`.
1483
+
1484
+ ## get
1485
+ ### `get(path, opts)`
1486
+ Igual que `MegaDB.get(path)`, con un nuevo parámetro opcional si MVCC está activado.
1487
+
1488
+ **`opts` (dict):** con MVCC habilitado, se reconocen:
1489
+ * `opts["$version"]` (int): solicita una versión específica.
1490
+ * `opts["$since"]` (timestamp ms): solicita el valor tal como existía en un momento dado.
1491
+ ```python
1492
+ db = MegaDBFull("data", {"mvcc": {"enabled": True, "retainVersions": 3}})
1493
+
1494
+ db.set("usuarios.pedro", {"edad": 20}) # internamente v2
1495
+ db.set("usuarios.pedro", {"edad": 21}) # internamente v1
1496
+ db.set("usuarios.pedro", {"edad": 22}) # internamente v0
1497
+
1498
+ print(db.get("usuarios.pedro")) # {'edad': 22}
1499
+ print(db.get("usuarios.pedro", {"$version": 1})) # {'edad': 21}
1500
+ import time
1501
+ print(db.get("usuarios.pedro", {"$since": time.time()*1000 - 5000})) # version vigente hace 5s
1502
+ ```
1503
+
1504
+ ## has / all / stream / entries / delete / keys / values / count / find / aggregate
1505
+ Igual que en `MegaDB`, con el mismo parámetro opcional `opts` (`$version`/`$since`) si MVCC está activado. Ver ejemplos análogos al de `get` arriba.
1506
+
1507
+ ## update
1508
+ ### `update(path, ops)`
1509
+ Igual que `MegaDB.update(path, ops)`.
1510
+
1511
+ ## watch
1512
+ ### `watch(path, callback)`
1513
+ Igual que `MegaDB.watch(path, callback)`.
1514
+
1515
+ ## history
1516
+ ### `history(path, limit)`
1517
+ Devuelve las últimas versiones (MVCC) de una clave exacta.
1518
+
1519
+ * **`path` (str):** ruta del documento (MVCC), debe haber sido agregada con [set](#set-2).
1520
+ * **`limit` (int):** número de versiones a devolver.
1521
+ * **Retorna:** una lista de `{"ts": ..., "value": ...}`.
1522
+ * Lanza `RuntimeError` si MVCC no está activado.
1523
+ ```python
1524
+ db = MegaDBFull("data", {"mvcc": {"enabled": True, "retainVersions": 3}})
1525
+
1526
+ db.set("usuarios", {"version": 2, "pedro": 14, "juan": 13})
1527
+ db.set("usuarios", {"version": 1, "maria": 15, "dix": 14})
1528
+ db.set("usuarios", {"version": 0, "maria": 15})
1529
+
1530
+ versiones = db.history("usuarios", 3)
1531
+ print(versiones)
1532
+ # [{'ts': ..., 'value': {'version': 0, 'maria': 15}}, {'ts': ..., 'value': {...v1}}, {'ts': ..., 'value': {...v2}}]
1533
+ ```
1534
+
1535
+ ## stats
1536
+ ### `stats()`
1537
+ Igual que `MegaDB.stats()`/`MegaDBSafe.stats()` pero con más información:
1538
+ * `appendOnly`: `enabled`, `file`, `sizeBytes`.
1539
+ * `mvcc`: `enabled`, `retainVersions`, `keysTracked`.
1540
+ * `indexes`: `composite`, `trie`, `skiplist`.
1541
+ * `merkle`: `enabled`, `root`.
1542
+ ```python
1543
+ # ejemplo db.stats()
1544
+ {
1545
+ "keys": 42, "dirtyBlocks": 3, "cachedBlocks": 5, "secondaryIndexes": 2,
1546
+ "keyCache": {"size": 12, "capacity": 100},
1547
+ "wal": {"file": "./db/wal.log", "pendingOps": 7},
1548
+ "appendOnly": {"enabled": True, "file": "./db/aof.log", "sizeBytes": 2048},
1549
+ "mvcc": {"enabled": True, "retainVersions": 3, "keysTracked": 10},
1550
+ "indexes": {"composite": {"001": 25}, "trie": 1, "skiplist": 2},
1551
+ "merkle": {"enabled": True, "root": "a9f3c8..."},
1552
+ "locks": {"queueLength": 0},
1553
+ }
1554
+ ```
1555
+
1556
+ ## flush / close / createBackup / restoreBackup / listBackups / createSnapshot / restoreSnapshot
1557
+ Igual que en `MegaDB`/`MegaDBSafe`.
1558
+
1559
+ ## verifyIntegrityFast
1560
+ ### `verifyIntegrityFast()`
1561
+ Comprueba rápidamente si los archivos del db (`block_*.json`, `aof.log`) no han sido modificados desde la última reconstrucción del árbol Merkle, compara hashes de hojas ya almacenados contra los hashes actuales de disco, sin recalcular el árbol completo desde cero cada vez.
1562
+
1563
+ * **Retorna:** `{"ok": bool, "expected": ..., "computed": ...}`.
1564
+ ```python
1565
+ ok = db.verifyIntegrityFast()
1566
+ print(ok)
1567
+ # {'ok': True, 'expected': 'a9f3c8...', 'computed': 'a9f3c8...'}
1568
+ # o si algo se corrompio:
1569
+ # {'ok': False, 'expected': 'a9f3c8...', 'computed': 'ff21bb...'}
1570
+ ```
1571
+
1572
+ ## rebuildAllIndexes
1573
+ ### `rebuildAllIndexes(flush_every=500)`
1574
+ Reconstruye **composite + trie + skiplist** (los que tengas configurados) en **un solo recorrido** de la colección, usando `entries()` internamente memoria acotada, sin cargar todos los documentos de una vez.
1575
+
1576
+ * **`flush_every`** (int, default `500`): cada cuántos documentos procesados se persisten a disco los shards/páginas modificados.
1577
+ * **Retorna:** `{"processed": ..., "indexes": [...]}`.
1578
+
1579
+ ```python
1580
+ db = MegaDBFull("productos", {
1581
+ "dir": "./",
1582
+ "compositeIndexes": [{"name": "cat_idx", "fields": ["categoria"]}],
1583
+ "compositePaged": True,
1584
+ "trieIndexes": ["nombre"], "triePaged": True,
1585
+ "skiplistIndexes": ["precio"], "skiplistPaged": True,
1586
+ })
1587
+
1588
+ # supongamos que la coleccion YA tenia 50,000 documentos antes de activar estos indices
1589
+ result = db.rebuildAllIndexes(flush_every=500)
1590
+ print(result) # {'processed': 50000, 'indexes': ['CompositeIndexManager', 'TrieIndexManager', 'SkipListIndexManager']}
1591
+ ```
1592
+ ---
1593
+
1594
+ # 4. `Transaction`
1595
+
1596
+ megadbx incluye un sistema de transacciones multi-base de datos que permite agrupar varias operaciones (set, delete, get, has) y aplicarlas de forma atómica, si algo falla, la transacción se revierte automáticamente.
1597
+
1598
+ #### Características principales
1599
+ - Soporte para múltiples bases de datos en una misma transacción.
1600
+ - Operaciones en memoria hasta que se confirme ([commit](#commit)).
1601
+ - Rollback automático en caso de error, o manual con [rollback](#rollback).
1602
+ - Compatible con WAL, AOF, QueuedLock, etc. (MegaDB, MegaDBSafe, MegaDBFull).
1603
+
1604
+ ## Transaction
1605
+ ```python
1606
+ from megadbx import Transaction
1607
+ operaciones = Transaction(dbs={})
1608
+ ```
1609
+
1610
+ - **dbs** (dict): mapa `{nombre: instancia_del_db}` (MegaDB, MegaDBSafe o MegaDBFull).
1611
+
1612
+ ```python
1613
+ from megadbx import MegaDB, Transaction
1614
+
1615
+ db1 = MegaDB("users", {"dir": "./"})
1616
+ db2 = MegaDB("games", {"dir": "./"})
1617
+
1618
+ tx = Transaction({"usuarios": db1, "juegos": db2})
1619
+
1620
+ # llamamos directamente con tx.usuarios. o tx.juegos.
1621
+ ```
1622
+ #### Cada base de datos dentro de la transacción tiene su propio proxy con:
1623
+ * `tx.<db>.get(path)` => igual que `MegaDB.get`
1624
+ * `tx.<db>.set(path, value)` => igual que `MegaDB.set`
1625
+ * `tx.<db>.has(path)` => igual que `MegaDB.has`
1626
+ * `tx.<db>.delete(path)` => igual que `MegaDB.delete`
1627
+
1628
+ #### Diferencia con usar la DB normal
1629
+ ##### Sin transacción:
1630
+ ```python
1631
+ db1.set('u123', {"name": "mega"})
1632
+ db2.set('o456', {"id": "u123", "item": "pc"})
1633
+ # si el segundo set falla, el primero ya quedó guardado: inconsistencia.
1634
+ ```
1635
+ ##### Con proxy dentro de una transacción:
1636
+ ```python
1637
+ tx.usuarios.set('u123', {"name": "mega"}) # se guarda en memoria
1638
+ tx.juegos.set('o456', {"id": "u123", "item": "pc"}) # también en memoria
1639
+
1640
+ try:
1641
+ status = tx.commit() # recién aquí se aplican juntos
1642
+ if status["ok"]:
1643
+ print("Se aplicaron las transacciones a la db real!")
1644
+ except Exception as error:
1645
+ print("no se hizo la transaccion, rollback automatico.", error)
1646
+ ```
1647
+ * Los cambios no tocan la DB real hasta que confirmes con [commit()](#commit).
1648
+ * Si algo falla antes del commit, puedes hacer [rollback()](#rollback) y nada cambia.
1649
+
1650
+ ### Ventajas:
1651
+ * Aísla los cambios: puedes leer dentro de la transacción y ver lo que modificaste, aunque todavía no esté en la DB real.
1652
+ * Seguridad: garantiza que los cambios se apliquen de forma atómica (todos o ninguno).
1653
+ * Compatibilidad: misma API (`get`/`set`/`delete`/`has`), pero bajo control transaccional.
1654
+
1655
+ ## commit
1656
+ ### `commit()`
1657
+ Aplica los cambios pendientes de memoria a la DB real. Es un **commit atómico real de 2 fases**:
1658
+
1659
+ 1. **Fase 1 (prepare)** antes de tocar ninguna DB, se escribe a disco (fsync, atómico) el payload completo: todos los writes/deletes ya resueltos a su valor final, para todas las DBs involucradas.
1660
+ 2. **Fase 2 (apply)** se aplican los cambios a cada DB.
1661
+ 3. **Fase 3 (commit)** el registro de la fase 1 se borra; su ausencia ES la marca de "esta transacción ya quedó aplicada".
1662
+
1663
+ Si el proceso muere entre la fase 1 y la fase 3, el registro de la fase 1 queda en disco. Al reiniciar, [`Transaction.recoverPending()`](#transactionrecoverPending-estático) lo encuentra y reaplica el payload, es seguro porque los valores guardados ya son el **valor final resuelto** (no deltas).
1664
+
1665
+ * **Retorna:** un dict con `{"ok": True}` si se aplicó correctamente.
1666
+
1667
+ ```python
1668
+ from megadbx import MegaDB, Transaction
1669
+
1670
+ db1 = MegaDB("users", {"dir": "./"})
1671
+ db2 = MegaDB("games", {"dir": "./"})
1672
+ tx = Transaction({"usuarios": db1, "juegos": db2})
1673
+
1674
+ tx.usuarios.set('u123', {"name": "mega"})
1675
+ tx.juegos.set('o456', {"id": "u123", "item": "pc"})
1676
+
1677
+ try:
1678
+ status = tx.commit()
1679
+ if status["ok"]:
1680
+ print("Se aplicaron las transacciones a la db real!")
1681
+ except Exception as error:
1682
+ print("no se hizo la transaccion, rollback automatico.", error)
1683
+ ```
1684
+
1685
+ ## rollback
1686
+ ### `rollback()`
1687
+ Revierte manualmente todos los cambios pendientes, o restaura el estado anterior si ya intentaste un `commit()` y algo falló.
1688
+ ```python
1689
+ tx = Transaction({"usuarios": db1, "juegos": db2})
1690
+
1691
+ tx.usuarios.set("u123", {"name": "mega", "balance": 500})
1692
+ tx.juegos.set("o456", {"userId": "u123", "item": "pc", "price": 300})
1693
+
1694
+ print("db real:", db1.get("u123")) # None
1695
+ print("tx transaccion:", tx.usuarios.get("u123")) # {'name': 'mega', 'balance': 500}
1696
+
1697
+ tx.rollback()
1698
+ print("db real tras rollback:", db1.get("u123")) # None
1699
+ print("tx transaccion tras rollback:", tx.usuarios.get("u123")) # None
1700
+ ```
1701
+
1702
+ ## Transaction.recoverPending (estático)
1703
+ ### `Transaction.recoverPending(dbs, opts={})`
1704
+ Reaplica cualquier transacción que haya quedado a medias por un crash del proceso anterior. **Llamalo una vez al arrancar la app**, despues de abrir tus bases de datos y antes de empezar a procesar. Es seguro llamarlo siempre, incluso si no hay nada pendiente.
1705
+
1706
+ * **`dbs`** (dict): mapa `{nombre: instancia}`.
1707
+ * **`opts["logDir"]`** (str, opcional): carpeta del log de transacciones. Por default, una carpeta hermana `_tx` junto a la primera DB del mapa.
1708
+ * **Retorna:** una lista de `{"txId": ..., "recovered": ...}` por cada transacción pendiente encontrada.
1709
+
1710
+ ```python
1711
+ from megadbx import MegaDB, Transaction
1712
+
1713
+ usuarios = MegaDB("usuarios", {"dir": "./"})
1714
+ pedidos = MegaDB("pedidos", {"dir": "./"})
1715
+
1716
+ # al arrancar la app:
1717
+ resultados = Transaction.recoverPending({"usuarios": usuarios, "pedidos": pedidos})
1718
+ print(resultados) # [] si no habia nada pendiente, o [{'txId': '...', 'recovered': True}, ...]
1719
+ ```
1720
+ ---
1721
+ # 5. `AdminPanel`
1722
+
1723
+ `AdminPanel` es un panel de administración web real: ver colecciones, buscar y paginar documentos, **crear/editar/borrar** con bloqueo optimista, gestionar índices y backups, revisar transacciones pendientes, y un registro de auditoría de cada acción (el panel requiere login obligatorio => configurable). Está construido sobre **Flask**.
1724
+
1725
+ ---
1726
+
1727
+ ## Qué se ve en la página?
1728
+ - **Barra lateral**: lista de colecciones registradas (con su conteo de documentos y un mini "mapa de bloques"), más dos secciones de sistema: **Transacciones pendientes** y **Registro de auditoría**.
1729
+ - **Vista de datos** (por colección): tabla paginada de documentos (clave + vista previa del valor), filtros avanzados, buscador que acepta la misma sintaxis de `query` que `find()` (ej: `{"edad": {"$gt": 18}}`), botón **+ Nuevo documento**, y por fila, **Editar**/**Borrar**.
1730
+ - **Pestaña Info**: info general de la colección + botón para correr `verifyIntegrityFast()`.
1731
+ - **Pestaña Metricas**: info general de todas mas metricas existentes en la base de datos, para monitoreo profundo.
1732
+ - **Pestaña Índices**: botón para ejecutar `rebuildAllIndexes()`.
1733
+ - **Pestaña Backups**: crear backup, listar backups existentes (con fecha y tamaño), restaurar uno (pide escribir `RESTAURAR` para confirmar).
1734
+ - **Editar un documento**: abre un editor de JSON crudo. Si el documento cambió en el servidor desde que lo abriste, el guardado se rechaza y muestra el valor actual en vez de pisarlo silenciosamente (bloqueo optimista).
1735
+ - **Borrar un documento**: pide escribir la clave exacta para confirmar.
1736
+
1737
+ Es una SPA en JavaScript plano (sin frameworks, sin CDNs externos).
1738
+
1739
+ ---
1740
+
1741
+ ## Seguridad, diseño y por qué
1742
+
1743
+ `AdminPanel` **no arranca sin credenciales**: si no le pasás `username`/`password`, tira un error en el constructor en vez de levantar un panel abierto por accidente. Es de un solo usuario (sin roles).
1744
+
1745
+ - **Autenticación**: usuario + contraseña (hasheada con `bcrypt`, nunca se persiste en texto plano). Sesión vía cookie `httpOnly` + `SameSite=Strict` (sesiones de Flask).
1746
+ - **CSRF**: se emite un token al loguearse; toda escritura (`POST`/`PUT`/`DELETE`) lo exige en el header `X-CSRF-Token`.
1747
+ - **Rate limiting de login**: 5 intentos fallidos cada 10 minutos por IP → `429`.
1748
+ - **Bind a `127.0.0.1` por default** si necesitas exponerlo en la red, pon un reverse proxy con TLS delante (nginx/caddy).
1749
+ - **CSP estricta** (`default-src 'self'`).
1750
+ - **El panel nunca toca el filesystem directo**: todas sus operaciones pasan por la API pública de MegaDB.
1751
+ - **Bloqueo optimista en ediciones**: cada lectura de un documento incluye un `etag` (hash del contenido). Al guardar, si el `etag` no coincide con el actual, se rechaza con `409`.
1752
+ - **Auditoría**: cada `create`/`edit`/`delete`/`login`/`rebuild_indexes`/`createBackup`/`restoreBackup` queda registrado (usa `AppendOnlyLog` internamente).
1753
+
1754
+ ---
1755
+
1756
+ ## AdminPanel
1757
+ ```python
1758
+ from megadbx import AdminPanel
1759
+ panel = AdminPanel(opts={})
1760
+ ```
1761
+
1762
+ Si usas esta clase, es obligatorio instalar estas dependencias (quedaron opcionales para no forzarlas en quien nunca use el panel):
1763
+ ```bash
1764
+ pip install megadbx[panel]
1765
+ # equivalente a: pip install flask bcrypt
1766
+ ```
1767
+
1768
+ - **opts** (dict):
1769
+
1770
+ | Opción | Default | Explicación |
1771
+ | -------------------------- | --------------------------------- | ------------------------------------------------------------------------ |
1772
+ | `dbs` | *(requerido)* | Mapa `{nombreColeccion: instanciaDeMegaDB}` las colecciones que administra el panel. |
1773
+ | `username` | *(requerido)* | Usuario para el login. Sin esto, el constructor tira error. |
1774
+ | `password` | *(requerido)* | Contraseña en texto plano (se hashea en memoria al llamar `start()`, nunca se persiste así). Mínimo 8 caracteres. |
1775
+ | `port` | `4850` | Puerto donde escucha el panel. |
1776
+ | `host` | `127.0.0.1` | A qué interfaz de red se enlaza. No lo cambies a `0.0.0.0` sin un proxy TLS delante. |
1777
+ | `sessionSecret` | *(aleatorio por arranque)* | Secret para firmar las cookies de sesión. Pasalo explícito si querés que las sesiones sobrevivan un reinicio. |
1778
+ | `cookieSecure` | `False` | `True` si el panel esta detras de un reverse proxy con HTTPS. |
1779
+ | `pageSize` | `50` | Documentos por página en la vista de datos. |
1780
+ | `maxPageSize` | `200` | Límite máximo de `limit` aceptado por query string. |
1781
+ | `sessionMaxAgeMs` | `28800000` ms (8h) | Duración de la sesión antes de requerir login de nuevo. |
1782
+ | `txLogDir` | carpeta hermana `_tx` | Dónde busca transacciones pendientes (ver [`Transaction.recoverPending`](#transactionrecoverPending-estático)). |
1783
+ | `auditDir` / `auditFile` | carpeta base / `panel_audit.log` | Dónde se guarda el registro de auditoría. |
1784
+
1785
+ ```python
1786
+ import os
1787
+ from megadbx import MegaDB, AdminPanel
1788
+
1789
+ usuarios = MegaDB("usuarios", {"dir": "./"})
1790
+ pedidos = MegaDB("pedidos", {"dir": "./"})
1791
+
1792
+ panel = AdminPanel({
1793
+ "dbs": {"usuarios": usuarios, "pedidos": pedidos}, # puedes registrar varias colecciones
1794
+ "username": "admin",
1795
+ "password": os.environ["PANEL_PASSWORD"],
1796
+ "port": 4850, # default
1797
+ })
1798
+
1799
+ panel.start()
1800
+ # [MegaDB Panel] abierto en http://127.0.0.1:4850
1801
+ ```
1802
+
1803
+ ## start
1804
+ ### `start()`
1805
+ Levanta el servidor Flask del panel (login, API, archivos estáticos del frontend) bloqueante, igual que `app.run()`. Es donde se hashea la contraseña y se registran todas las rutas.
1806
+ ```python
1807
+ panel.start()
1808
+ ```
1809
+ > Para producción, o si necesitas correr el panel junto a otro código en el mismo proceso, usa `panel.build_app()` en su lugar: devuelve la app de Flask sin arrancar un servidor, para servirla con `gunicorn`/`waitress`, o para testear con el `test_client()` de Flask.
1810
+
1811
+ ## stop
1812
+ ### `stop()`
1813
+ Detiene el servidor del panel (el servidor de desarrollo de Flask no siempre permite un shutdown limpio desde otro hilo, para eso, usar WSGI server de producción y usar los propios mecanismos de shutdown).
1814
+ ```python
1815
+ panel.stop()
1816
+ ```
1817
+
1818
+ ---
1819
+
1820
+ # Dato importante:
1821
+ ## Leer después de entender MegaDB, MegaDBSafe y MegaDBFull.
1822
+ ### Uso correcto de instancias de megadbx (MegaDB, MegaDBSafe, MegaDBFull)
1823
+
1824
+ - **Si creas una base de datos con `MegaDB(...)`, `MegaDBSafe(...)` o `MegaDBFull(...)` dentro de un mismo módulo y la usas solo ahí**, esta bien.
1825
+ - **Si necesitas usar la misma base en varios módulos**, NO vuelvas a llamar al constructor en cada uno: eso crea **múltiples instancias independientes** que duplican carga, memoria, procesos internos, y pueden provocar **inconsistencias** o conflictos de escritura (WAL, AOF, etc.).
1826
+ - Solución recomendable: **crear la instancia una vez** y **reutilizarla** (patrón singleton/módulo) o usar el modo [Multiproceso](#multiproceso).
1827
+
1828
+ ### ¿Por qué es un problema instanciar varias veces?
1829
+ Cuando haces `MegaDB("users", ...)`:
1830
+ - Se crea una **instancia en memoria**.
1831
+ - Esa instancia **carga** datos desde disco, prepara los bloques, sistemas internos, WAL/AOF, locks, colas internas, etc.
1832
+ - Si en otro módulo volvés a hacer `MegaDB("users", ...)`, obtenés otra instancia que vuelve a cargar y mantener su propio estado en memoria.
1833
+
1834
+ Efectos negativos:
1835
+ - **Doble I/O** y mayor uso de memoria.
1836
+ - **Riesgo de inconsistencias**: dos instancias distintas pueden tener una vista distinta del estado en memoria.
1837
+ - **Conflictos** en escrituras concurrentes al mismo archivo.
1838
+ - **Comportamientos no deseados** en índices, locks o procesos en segundo plano.
1839
+
1840
+ ### Qué hacer si no se usa el modo Multiproceso:
1841
+
1842
+ #### 1) Exportar una instancia única (simple y efectiva)
1843
+ En Python, un módulo se ejecuta una sola vez e `import` reutiliza la misma instancia, así que este patron es directo:
1844
+ ```python
1845
+ # archivo db.py
1846
+ from megadbx import MegaDB
1847
+
1848
+ usuarios_db = MegaDB('users', {"dir": "./"})
1849
+ mascotas_db = MegaDB('mascotas', {"dir": "./"})
1850
+ ```
1851
+ Lo usas desde otro módulo:
1852
+ ```python
1853
+ # comando1.py
1854
+ from db import usuarios_db, mascotas_db
1855
+
1856
+ usuarios_db.set('u1', {"name": "Alice"})
1857
+ mascotas_db.set(...)
1858
+ ```
1859
+
1860
+ #### 2) Factory / Singleton central (gestor que devuelve instancias)
1861
+ Útil si vas a tener muchas colecciones o distintos tipos (MegaDB, MegaDBSafe, MegaDBFull):
1862
+ ```python
1863
+ # manager.py
1864
+ class DBManager:
1865
+ def __init__(self):
1866
+ self._instances = {}
1867
+
1868
+ def get_db(self, type_class, name, options=None):
1869
+ options = options or {}
1870
+ key = f"{type_class.__name__}:{name}"
1871
+ if key in self._instances:
1872
+ return self._instances[key]
1873
+
1874
+ db = type_class(name, options) # creamos instancia (ya lista al retornar, es sincrona)
1875
+ self._instances[key] = db
1876
+ return db
1877
+
1878
+ manager = DBManager() # exportamos singleton
1879
+ ```
1880
+ Lo llamás desde otros módulos:
1881
+ ```python
1882
+ # comando1.py
1883
+ from manager import manager
1884
+ from megadbx import MegaDB, MegaDBSafe, MegaDBFull
1885
+
1886
+ usuarios_db = manager.get_db(MegaDB, "users", {"dir": "./"})
1887
+ mascotas_db = manager.get_db(MegaDBSafe, "mascotas", {"dir": "./"})
1888
+ profesion_db = manager.get_db(MegaDBFull, "profesiones", {"dir": "./"})
1889
+
1890
+ print("Todas las instancias estan listas")
1891
+
1892
+ usuarios_db.set("mega", {"id": "00001", "coins": 10000})
1893
+ print(usuarios_db.get("mega"))
1894
+
1895
+ mascotas_db.set("perro002", {"owner": "mega", "age": 3})
1896
+ print(mascotas_db.get("perro002"))
1897
+ ```
1898
+ ##### Ventajas de este patrón singleton
1899
+ * Una sola instancia por DB: si en otro módulo llamás `get_db` con los mismos parámetros, recibís la misma instancia ya inicializada.
1900
+ * Un único proceso.
1901
+ * No hay conflicto: puedes tener varias bases diferentes en el mismo proyecto.
1902
+ * Soporte para todas las variantes: funciona igual con MegaDB, MegaDBSafe y MegaDBFull.
1903
+ * Clave única por tipo + nombre.
1904
+ * Recordatorio: megadbx actualmente soporta [multiprocesos, asi puedes usar la misma base de datos simultaneamente en diferentes nodos, procesos, etc](#multiproceso)