Mini MongoDB: consultas, proyecciones y actualizaciones sobre JSON
Programa el motor de consultas de una base de datos documental como MongoDB: filtros con igualdad y operadores ($gt, $in, $regex, $exists, $size, $or…), notación de punto, arrays, proyecciones, sort con skip y limit, distinct y actualizaciones con $set, $inc, $push y $pull.
- Bases de datos documentales
- Filtros y operadores de consulta
- Arrays y notación de punto
- Proyección, sort, skip y limit
- Operadores de actualización
- Recursividad sobre objetos JSON
Enunciado
En MongoDB los datos son documentos JSON dentro de colecciones, sin esquema fijo: un alumno puede tener aficiones y otro no. Las consultas también son JSON: {"edad": {"$gte": 18}, "ciudad": "Madrid"} busca los mayores de edad de Madrid. Escribir el motor que las resuelve es la mejor forma de entender su semántica, que tiene detalles que caen en los exámenes: un filtro sobre un campo array se cumple si lo cumple cualquiera de sus elementos, null encuentra también los documentos sin ese campo y sort, skip y limit se aplican siempre en ese orden, se encadenen como se encadenen.
La entrada empieza con el nombre de la colección, sigue con un documento JSON por línea (como los exporta mongoexport), una línea --- y las órdenes, con la sintaxis de mongosh pero en JSON estricto (claves entre comillas dobles). El código de partida ya lee la entrada, separa las llamadas encadenadas y ejecuta cada orden: tú programas el filtro, la proyección, la ordenación y las actualizaciones.
Qué tiene que hacer el programa
coincide(doc, filtro): cada clave del filtro debe cumplirse. Una clave con punto entra en subdocumentos (notas.prog). Un valor normal es igualdad: si el campo es un array, basta con que el array entero o uno de sus elementos sea igual;nullcoincide con null y con un campo que no existe.$andy$orreciben una lista de filtros. Cualquier otra clave con$al principio:operador desconocido: $x.- Operadores:
$eq,$ne(ninguno de los elementos es igual),$gt,$gte,$lt,$lte(solo comparan valores del mismo tipo, concomparar; en un array, basta un elemento),$iny$nin(con una lista;$nintambién coincide si el campo no existe),$exists,$regexcon$optionsopcional (en un array, basta un elemento de texto),$sizey$not(niega otro objeto de operadores). Uno desconocido:operador desconocido: $x. proyectar(doc, proy): con unos ({"nombre": 1}) solo esos campos, con ceros ({"aficiones": 0}) todos menos esos; el_idsale siempre salvo con"_id": 0, y{"_id": 1}solo deja el_id. Los campos, en el orden del documento. Mezclar 1 y 0 (sin contar el_id):proyección no válida: no se pueden mezclar 1 y 0.ordenar(docs, orden): por cada campo del objeto, 1 ascendente y -1 descendente, concomparar(que ordena como MongoDB: nulos y campos que no existen primero, después números, textos…). Ordenación estable.aplicar(doc, update)devuelve una copia actualizada:$set(crea los subdocumentos que falten),$unset,$inc(si no existe, lo crea; errores$inc necesita un númeroy$inc: campo no es un número),$push(si no existe, crea el array; error$push: campo no es un array) y$pull(quita los elementos iguales). Cambiar el_id:no se puede cambiar el _id; una clave sin$:una actualización necesita operadores ($set, $inc...).
Entrada
Línea 1: el nombre de la colección.
Después, un documento JSON por línea, hasta una línea ---.
Después, una orden por línea: db.coleccion.find(filtro, proyeccion).sort(...).skip(n).limit(n), countDocuments, distinct, updateOne, updateMany, deleteOne, deleteMany o insertOne.
Datos de referencia
| _id | nombre | edad | ciudad | aficiones | notas |
|---|---|---|---|---|---|
| 1 | Ana | 19 | Madrid | ["ajedrez", "cine"] | {"prog": 8, "bd": 9} |
| 2 | Luis | 17 | Sevilla | ["fútbol"] | {"prog": 5, "bd": 4} |
| 3 | Eva | 22 | Madrid | [] | {"prog": 9, "bd": 7} |
| 4 | Iker | 18 | Bilbao | ["cine", "fútbol"] | {"prog": 6, "bd": null} |
| 5 | Sara | 20 | Sevilla | (no tiene) | {"prog": 7, "bd": 8} |
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Consultas sobre alumnos
Entrada
alumnos
{"_id": 1, "nombre": "Ana", "edad": 19, "ciudad": "Madrid", "aficiones": ["ajedrez", "cine"], "notas": {"prog": 8, "bd": 9}}
{"_id": 2, "nombre": "Luis", "edad": 17, "ciudad": "Sevilla", "aficiones": ["fútbol"], "notas": {"prog": 5, "bd": 4}}
{"_id": 3, "nombre": "Eva", "edad": 22, "ciudad": "Madrid", "aficiones": [], "notas": {"prog": 9, "bd": 7}}
{"_id": 4, "nombre": "Iker", "edad": 18, "ciudad": "Bilbao", "aficiones": ["cine", "fútbol"], "notas": {"prog": 6, "bd": null}}
{"_id": 5, "nombre": "Sara", "edad": 20, "ciudad": "Sevilla", "notas": {"prog": 7, "bd": 8}}
---
db.alumnos.find({"ciudad": "Madrid"})
db.alumnos.find({"edad": {"$gte": 18, "$lt": 21}}, {"nombre": 1, "edad": 1, "_id": 0}).sort({"edad": -1})
db.alumnos.find({"aficiones": "cine"}, {"nombre": 1})
db.alumnos.find({"notas.prog": {"$gte": 7}}, {"notas": 0}).limit(2).sort({"notas.prog": -1})
db.alumnos.countDocuments({"aficiones": {"$exists": false}})
db.alumnos.distinct("aficiones")
db.alumnos.find({"$or": [{"ciudad": "Bilbao"}, {"aficiones": {"$size": 0}}]}, {"nombre": 1, "_id": 0})
db.alumnos.find({"notas.bd": null}, {"_id": 1})Salida por consola
Colección alumnos: 5 documentos
> db.alumnos.find({"ciudad": "Madrid"})
{"_id":1,"nombre":"Ana","edad":19,"ciudad":"Madrid","aficiones":["ajedrez","cine"],"notas":{"prog":8,"bd":9}}
{"_id":3,"nombre":"Eva","edad":22,"ciudad":"Madrid","aficiones":[],"notas":{"prog":9,"bd":7}}
> db.alumnos.find({"edad": {"$gte": 18, "$lt": 21}}, {"nombre": 1, "edad": 1, "_id": 0}).sort({"edad": -1})
{"nombre":"Sara","edad":20}
{"nombre":"Ana","edad":19}
{"nombre":"Iker","edad":18}
> db.alumnos.find({"aficiones": "cine"}, {"nombre": 1})
{"_id":1,"nombre":"Ana"}
{"_id":4,"nombre":"Iker"}
> db.alumnos.find({"notas.prog": {"$gte": 7}}, {"notas": 0}).limit(2).sort({"notas.prog": -1})
{"_id":3,"nombre":"Eva","edad":22,"ciudad":"Madrid","aficiones":[]}
{"_id":1,"nombre":"Ana","edad":19,"ciudad":"Madrid","aficiones":["ajedrez","cine"]}
> db.alumnos.countDocuments({"aficiones": {"$exists": false}})
1
> db.alumnos.distinct("aficiones")
["ajedrez","cine","fútbol"]
> db.alumnos.find({"$or": [{"ciudad": "Bilbao"}, {"aficiones": {"$size": 0}}]}, {"nombre": 1, "_id": 0})
{"nombre":"Eva"}
{"nombre":"Iker"}
> db.alumnos.find({"notas.bd": null}, {"_id": 1})
{"_id":4}Actualizaciones, borrados y errores
Entrada
alumnos
{"_id": 1, "nombre": "Ana", "edad": 19, "ciudad": "Madrid", "aficiones": ["ajedrez", "cine"], "notas": {"prog": 8, "bd": 9}}
{"_id": 2, "nombre": "Luis", "edad": 17, "ciudad": "Sevilla", "aficiones": ["fútbol"], "notas": {"prog": 5, "bd": 4}}
{"_id": 3, "nombre": "Eva", "edad": 22, "ciudad": "Madrid", "aficiones": [], "notas": {"prog": 9, "bd": 7}}
{"_id": 4, "nombre": "Iker", "edad": 18, "ciudad": "Bilbao", "aficiones": ["cine", "fútbol"], "notas": {"prog": 6, "bd": null}}
{"_id": 5, "nombre": "Sara", "edad": 20, "ciudad": "Sevilla", "notas": {"prog": 7, "bd": 8}}
---
db.alumnos.updateMany({"ciudad": "Sevilla"}, {"$inc": {"edad": 1}, "$push": {"aficiones": "teatro"}})
db.alumnos.find({"ciudad": "Sevilla"}, {"nombre": 1, "edad": 1, "aficiones": 1, "_id": 0})
db.alumnos.updateOne({"nombre": "Ana"}, {"$set": {"notas.prog": 10, "beca": true}, "$pull": {"aficiones": "cine"}})
db.alumnos.find({"beca": true})
db.alumnos.updateMany({}, {"$set": {"_id": 9}})
db.alumnos.deleteMany({"edad": {"$lt": 18}})
db.alumnos.insertOne({"nombre": "Nora", "edad": 30})
db.alumnos.insertOne({"_id": 2, "nombre": "Repe"})
db.profes.find({})
db.alumnos.find({"edad": {"$entre": [18, 20]}})
db.alumnos.find({"nombre": 'Ana'})
db.alumnos.aggregate([])
db.alumnos.find({}, {"nombre": 1, "edad": 0})
db.alumnos.find({}, {"nombre": 1, "_id": 0}).sort({"edad": 1}).skip(1).limit(3)Salida por consola
Colección alumnos: 5 documentos
> db.alumnos.updateMany({"ciudad": "Sevilla"}, {"$inc": {"edad": 1}, "$push": {"aficiones": "teatro"}})
{"matchedCount":2,"modifiedCount":2}
> db.alumnos.find({"ciudad": "Sevilla"}, {"nombre": 1, "edad": 1, "aficiones": 1, "_id": 0})
{"nombre":"Luis","edad":18,"aficiones":["fútbol","teatro"]}
{"nombre":"Sara","edad":21,"aficiones":["teatro"]}
> db.alumnos.updateOne({"nombre": "Ana"}, {"$set": {"notas.prog": 10, "beca": true}, "$pull": {"aficiones": "cine"}})
{"matchedCount":1,"modifiedCount":1}
> db.alumnos.find({"beca": true})
{"_id":1,"nombre":"Ana","edad":19,"ciudad":"Madrid","aficiones":["ajedrez"],"notas":{"prog":10,"bd":9},"beca":true}
> db.alumnos.updateMany({}, {"$set": {"_id": 9}})
Error: no se puede cambiar el _id
> db.alumnos.deleteMany({"edad": {"$lt": 18}})
{"deletedCount":0}
> db.alumnos.insertOne({"nombre": "Nora", "edad": 30})
{"insertedId":6}
> db.alumnos.insertOne({"_id": 2, "nombre": "Repe"})
Error: _id duplicado: 2
> db.profes.find({})
Error: la colección «profes» no existe
> db.alumnos.find({"edad": {"$entre": [18, 20]}})
Error: operador desconocido: $entre
> db.alumnos.find({"nombre": 'Ana'})
Error: JSON no válido
> db.alumnos.aggregate([])
Error: método desconocido: aggregate
> db.alumnos.find({}, {"nombre": 1, "edad": 0})
Error: proyección no válida: no se pueden mezclar 1 y 0
> db.alumnos.find({}, {"nombre": 1, "_id": 0}).sort({"edad": 1}).skip(1).limit(3)
{"nombre":"Iker"}
{"nombre":"Ana"}
{"nombre":"Sara"}Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. Igualdad con arrays
La clave de MongoDB: {"aficiones": "cine"} encuentra a quien tiene cine entre sus aficiones. Prueba el valor contra el array entero y contra cada elemento.
const candidatos = (v) => (Array.isArray(v) ? [v, ...v] : [v]);
const igual = (valor, esperado) =>
candidatos(valor).some((v) => JSON.stringify(v) === JSON.stringify(esperado));2. ¿Operadores o valor?
Un objeto cuyas claves empiezan todas por $ son operadores ({"$gte": 18}); cualquier otro objeto es un valor que se compara entero (un subdocumento exacto).
3. Un switch por operador
Object.entries(ops).every(...) exige que se cumplan todos los operadores del objeto, y un switch resuelve cada uno. $not es recursivo: vuelve a llamar a la misma función.
4. Actualizar sin romper
Trabaja sobre una copia (JSON.parse(JSON.stringify(doc))): si un operador falla a medias, el original queda intacto. El código de partida solo sustituye los documentos si todas las copias salen bien.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.
Ejemplo
alumnos
{"_id": 1, "nombre": "Ana", "edad": 19, "ciudad": "Madrid", "aficiones": ["ajedrez", "cine"], "notas": {"prog": 8, "bd": 9}}
{"_id": 2, "nombre": "Luis", "edad": 17, "ciudad": "Sevilla", "aficiones": ["fútbol"], "notas": {"prog": 5, "bd": 4}}
{"_id": 3, "nombre": "Eva", "edad": 22, "ciudad": "Madrid", "aficiones": [], "notas": {"prog": 9, "bd": 7}}
{"_id": 4, "nombre": "Iker", "edad": 18, "ciudad": "Bilbao", "aficiones": ["cine", "fútbol"], "notas": {"prog": 6, "bd": null}}
{"_id": 5, "nombre": "Sara", "edad": 20, "ciudad": "Sevilla", "notas": {"prog": 7, "bd": 8}}
---
db.alumnos.find({"ciudad": "Madrid"})
db.alumnos.find({"edad": {"$gte": 18, "$lt": 21}}, {"nombre": 1, "edad": 1, "_id": 0}).sort({"edad": -1})
db.alumnos.find({"aficiones": "cine"}, {"nombre": 1})
db.alumnos.find({"notas.prog": {"$gte": 7}}, {"notas": 0}).limit(2).sort({"notas.prog": -1})
db.alumnos.countDocuments({"aficiones": {"$exists": false}})
db.alumnos.distinct("aficiones")
db.alumnos.find({"$or": [{"ciudad": "Bilbao"}, {"aficiones": {"$size": 0}}]}, {"nombre": 1, "_id": 0})
db.alumnos.find({"notas.bd": null}, {"_id": 1})
Colección alumnos: 5 documentos
> db.alumnos.find({"ciudad": "Madrid"})
{"_id":1,"nombre":"Ana","edad":19,"ciudad":"Madrid","aficiones":["ajedrez","cine"],"notas":{"prog":8,"bd":9}}
{"_id":3,"nombre":"Eva","edad":22,"ciudad":"Madrid","aficiones":[],"notas":{"prog":9,"bd":7}}
> db.alumnos.find({"edad": {"$gte": 18, "$lt": 21}}, {"nombre": 1, "edad": 1, "_id": 0}).sort({"edad": -1})
{"nombre":"Sara","edad":20}
{"nombre":"Ana","edad":19}
{"nombre":"Iker","edad":18}
> db.alumnos.find({"aficiones": "cine"}, {"nombre": 1})
{"_id":1,"nombre":"Ana"}
{"_id":4,"nombre":"Iker"}
> db.alumnos.find({"notas.prog": {"$gte": 7}}, {"notas": 0}).limit(2).sort({"notas.prog": -1})
{"_id":3,"nombre":"Eva","edad":22,"ciudad":"Madrid","aficiones":[]}
{"_id":1,"nombre":"Ana","edad":19,"ciudad":"Madrid","aficiones":["ajedrez","cine"]}
> db.alumnos.countDocuments({"aficiones": {"$exists": false}})
1
> db.alumnos.distinct("aficiones")
["ajedrez","cine","fútbol"]
> db.alumnos.find({"$or": [{"ciudad": "Bilbao"}, {"aficiones": {"$size": 0}}]}, {"nombre": 1, "_id": 0})
{"nombre":"Eva"}
{"nombre":"Iker"}
> db.alumnos.find({"notas.bd": null}, {"_id": 1})
{"_id":4}Solución explicada
Ver la solución completa
1const lineas = require("fs").readFileSync(0, "utf8").split("\n");
2
3// ── Orden de los valores como en MongoDB (BSON): nulos < números < textos < objetos < arrays < booleanos
4function rango(v) {
5 if (v === undefined || v === null) return 0;
6 if (typeof v === "number") return 1;
7 if (typeof v === "string") return 2;
8 if (Array.isArray(v)) return 4;
9 if (typeof v === "object") return 3;
10 return 5;
11}
12
13function comparar(a, b) {
14 const ra = rango(a), rb = rango(b);
15 if (ra !== rb) return ra - rb;
16 if (ra === 1 || ra === 5) return Number(a) - Number(b);
17 const ta = ra === 2 ? a : JSON.stringify(a), tb = ra === 2 ? b : JSON.stringify(b);
18 return ta < tb ? -1 : ta > tb ? 1 : 0;
19}
20
21const igualProfundo = (a, b) => JSON.stringify(a) === JSON.stringify(b);
22
23/** Valor de un campo con notación de punto ("notas.prog"); undefined si no existe. */
24function obtener(doc, ruta) {
25 let v = doc;
26 for (const parte of ruta.split(".")) {
27 if (v === null || typeof v !== "object" || Array.isArray(v)) return undefined;
28 v = v[parte];
29 }
30 return v;
31}
32
33const esOperadores = (c) =>
34 c !== null && typeof c === "object" && !Array.isArray(c) && Object.keys(c).length > 0 && Object.keys(c).every((k) => k.startsWith("quot;));
35
36// Si el campo es un array, la condición se cumple con el array entero o con cualquiera de sus elementos
37const candidatos = (v) => (Array.isArray(v) ? [v, ...v] : [v]);
38
39function igual(valor, esperado) {
40 if (esperado === null) return valor === null || valor === undefined;
41 return candidatos(valor).some((v) => igualProfundo(v, esperado));
42}
43
44// $gt, $lt...: solo se comparan valores del mismo tipo (un número nunca es mayor que un texto)
45const comparable = (valor, arg, ok) => candidatos(valor).some((v) => v !== undefined && v !== null && rango(v) === rango(arg) && ok(comparar(v, arg)));
46
47function cumple(valor, ops) {
48 return Object.entries(ops).every(([op, arg]) => {
49 switch (op) {
50 case "$eq": return igual(valor, arg);
51 case "$ne": return !igual(valor, arg);
52 case "$gt": return comparable(valor, arg, (c) => c > 0);
53 case "$gte": return comparable(valor, arg, (c) => c >= 0);
54 case "$lt": return comparable(valor, arg, (c) => c < 0);
55 case "$lte": return comparable(valor, arg, (c) => c <= 0);
56 case "$in":
57 case "$nin": {
58 if (!Array.isArray(arg)) throw new Error(`${op} necesita un array`);
59 const alguno = arg.some((a) => igual(valor, a));
60 return op === "$in" ? alguno : !alguno;
61 }
62 case "$exists": return (valor !== undefined) === Boolean(arg);
63 case "$regex": {
64 const re = new RegExp(arg, ops.$options ?? "");
65 return candidatos(valor).some((v) => typeof v === "string" && re.test(v));
66 }
67 case "$options": return true;
68 case "$size": return Array.isArray(valor) && valor.length === arg;
69 case "$not": return !cumple(valor, arg);
70 default: throw new Error(`operador desconocido: ${op}`);
71 }
72 });
73}
74
75function coincide(doc, filtro) {
76 return Object.entries(filtro).every(([clave, cond]) => {
77 if (clave === "$and") return cond.every((f) => coincide(doc, f));
78 if (clave === "$or") return cond.some((f) => coincide(doc, f));
79 if (clave.startsWith("quot;)) throw new Error(`operador desconocido: ${clave}`);
80 const valor = obtener(doc, clave);
81 return esOperadores(cond) ? cumple(valor, cond) : igual(valor, cond);
82 });
83}
84
85function proyectar(doc, proy) {
86 const campos = Object.keys(proy ?? {});
87 if (campos.length === 0) return doc;
88 const otros = campos.filter((k) => k !== "_id");
89 const modos = new Set(otros.map((k) => Boolean(proy[k])));
90 if (modos.size > 1) throw new Error("proyección no válida: no se pueden mezclar 1 y 0");
91 const incluir = otros.length ? modos.has(true) : Boolean(proy._id);
92 const r = {};
93 for (const [k, v] of Object.entries(doc)) {
94 const dentro = k === "_id" ? proy._id === undefined || Boolean(proy._id) : incluir ? Boolean(proy[k]) : proy[k] === undefined;
95 if (dentro) r[k] = v;
96 }
97 return r;
98}
99
100function ordenar(docs, orden) {
101 return [...docs].sort((a, b) => {
102 for (const [campo, dir] of Object.entries(orden)) {
103 const c = comparar(obtener(a, campo), obtener(b, campo));
104 if (c !== 0) return dir < 0 ? -c : c;
105 }
106 return 0;
107 });
108}
109
110/** Aplica los operadores de actualización a una copia del documento; devuelve la copia. */
111function aplicar(doc, update) {
112 const copia = JSON.parse(JSON.stringify(doc));
113 for (const [op, campos] of Object.entries(update)) {
114 if (!op.startsWith("quot;)) throw new Error("una actualización necesita operadores ($set, $inc...)");
115 for (const [ruta, valor] of Object.entries(campos)) {
116 if (ruta === "_id" || ruta.startsWith("_id.")) throw new Error("no se puede cambiar el _id");
117 const partes = ruta.split(".");
118 const ultima = partes.pop();
119 let o = copia;
120 for (const p of partes) {
121 if (o[p] === undefined) o[p] = {};
122 if (o[p] === null || typeof o[p] !== "object" || Array.isArray(o[p])) throw new Error(`${ruta}: ${p} no es un subdocumento`);
123 o = o[p];
124 }
125 switch (op) {
126 case "$set": o[ultima] = valor; break;
127 case "$unset": delete o[ultima]; break;
128 case "$inc":
129 if (typeof valor !== "number") throw new Error("$inc necesita un número");
130 if (o[ultima] === undefined) o[ultima] = valor;
131 else if (typeof o[ultima] !== "number") throw new Error(`$inc: ${ruta} no es un número`);
132 else o[ultima] += valor;
133 break;
134 case "$push":
135 if (o[ultima] === undefined) o[ultima] = [valor];
136 else if (!Array.isArray(o[ultima])) throw new Error(`$push: ${ruta} no es un array`);
137 else o[ultima].push(valor);
138 break;
139 case "$pull":
140 if (Array.isArray(o[ultima])) o[ultima] = o[ultima].filter((x) => !igualProfundo(x, valor));
141 break;
142 default: throw new Error(`operador desconocido: ${op}`);
143 }
144 }
145 }
146 return copia;
147}
148
149/** ".find({...}).sort({...})" → [{ metodo: "find", args: [...] }, { metodo: "sort", args: [...] }] */
150function llamadas(texto) {
151 const r = [];
152 let i = 0;
153 while (i < texto.length) {
154 const m = /^\.(\w+)\(/.exec(texto.slice(i));
155 if (!m) throw new Error("sintaxis no válida");
156 i += m[0].length;
157 let nivel = 1, enTexto = false, j = i;
158 for (; j < texto.length && nivel > 0; j++) {
159 const c = texto[j];
160 if (enTexto) {
161 if (c === "\\") j++;
162 else if (c === '"') enTexto = false;
163 } else if (c === '"') enTexto = true;
164 else if (c === "(") nivel++;
165 else if (c === ")") nivel--;
166 }
167 if (nivel > 0) throw new Error("falta cerrar un paréntesis");
168 let args;
169 try {
170 args = JSON.parse("[" + texto.slice(i, j - 1) + "]");
171 } catch {
172 throw new Error("JSON no válido");
173 }
174 r.push({ metodo: m[1], args });
175 i = j;
176 }
177 return r;
178}
179
180const json = (v) => JSON.stringify(v);
181
182function ejecutar(col, nombre, linea) {
183 const m = /^db\.(\w+)(\..*)$/.exec(linea);
184 if (!m) throw new Error("las órdenes empiezan por db.coleccion.");
185 if (m[1] !== nombre) throw new Error(`la colección «${m[1]}» no existe`);
186 const [primera, ...resto] = llamadas(m[2]);
187 const { metodo, args } = primera;
188 if (resto.length && metodo !== "find") throw new Error(`${resto[0].metodo} solo se puede encadenar a find`);
189 const filtro = args[0] ?? {};
190 switch (metodo) {
191 case "find": {
192 let docs = col.filter((d) => coincide(d, filtro));
193 const mod = {};
194 for (const { metodo: mm, args: a } of resto) {
195 if (!["sort", "skip", "limit"].includes(mm)) throw new Error(`método desconocido: ${mm}`);
196 mod[mm] = a[0];
197 }
198 // da igual el orden en que se encadenen: siempre sort, después skip y después limit
199 if (mod.sort) docs = ordenar(docs, mod.sort);
200 if (mod.skip) docs = docs.slice(mod.skip);
201 if (mod.limit) docs = docs.slice(0, mod.limit);
202 docs = docs.map((d) => proyectar(d, args[1]));
203 if (docs.length === 0) console.log("(ningún documento)");
204 for (const d of docs) console.log(json(d));
205 return;
206 }
207 case "countDocuments":
208 console.log(col.filter((d) => coincide(d, filtro)).length);
209 return;
210 case "distinct": {
211 const valores = [];
212 for (const d of col.filter((x) => coincide(x, args[1] ?? {}))) {
213 const v = obtener(d, String(args[0]));
214 for (const x of Array.isArray(v) ? v : [v]) {
215 if (x !== undefined && !valores.some((y) => igualProfundo(x, y))) valores.push(x);
216 }
217 }
218 console.log(json(valores.sort(comparar)));
219 return;
220 }
221 case "updateOne":
222 case "updateMany": {
223 if (!args[1] || typeof args[1] !== "object") throw new Error(`${metodo} necesita un filtro y una actualización`);
224 let indices = col.flatMap((d, i) => (coincide(d, filtro) ? [i] : []));
225 if (metodo === "updateOne") indices = indices.slice(0, 1);
226 // primero todas las copias: si una falla, no se cambia ningún documento
227 const nuevos = indices.map((i) => aplicar(col[i], args[1]));
228 let modificados = 0;
229 indices.forEach((i, k) => {
230 if (!igualProfundo(col[i], nuevos[k])) modificados++;
231 col[i] = nuevos[k];
232 });
233 console.log(json({ matchedCount: indices.length, modifiedCount: modificados }));
234 return;
235 }
236 case "deleteOne":
237 case "deleteMany": {
238 let borrar = col.filter((d) => coincide(d, filtro));
239 if (metodo === "deleteOne") borrar = borrar.slice(0, 1);
240 for (const d of borrar) col.splice(col.indexOf(d), 1);
241 console.log(json({ deletedCount: borrar.length }));
242 return;
243 }
244 case "insertOne": {
245 let doc = args[0];
246 if (doc === null || typeof doc !== "object" || Array.isArray(doc)) throw new Error("insertOne necesita un documento");
247 if (doc._id === undefined) {
248 const ids = col.map((d) => d._id).filter((n) => typeof n === "number");
249 doc = { _id: ids.length ? Math.max(...ids) + 1 : 1, ...doc };
250 } else if (col.some((d) => igualProfundo(d._id, doc._id))) {
251 throw new Error(`_id duplicado: ${json(doc._id)}`);
252 }
253 col.push(doc);
254 console.log(json({ insertedId: doc._id }));
255 return;
256 }
257 default:
258 throw new Error(`método desconocido: ${metodo}`);
259 }
260}
261
262function main() {
263 const nombre = (lineas[0] ?? "").trim();
264 const col = [];
265 let i = 1;
266 for (; i < lineas.length && lineas[i].trim() !== "---"; i++) {
267 if (!lineas[i].trim()) continue;
268 try {
269 const doc = JSON.parse(lineas[i]);
270 if (doc === null || typeof doc !== "object" || Array.isArray(doc)) throw new Error();
271 col.push(doc);
272 } catch {
273 console.log(`Línea ${i + 1}: documento no válido, se ignora`);
274 }
275 }
276 console.log(`Colección ${nombre}: ${col.length} documentos`);
277 for (i++; i < lineas.length; i++) {
278 const linea = lineas[i].trim();
279 if (!linea) continue;
280 console.log("> " + linea);
281 try {
282 ejecutar(col, nombre, linea);
283 } catch (e) {
284 console.log("Error: " + e.message);
285 }
286 }
287}
288
289main();Que un filtro sobre un array mire sus elementos es lo que hace cómodas las bases documentales: los datos que en relacional irían en otra tabla (las aficiones de cada alumno) se guardan dentro del documento y se consultan igual que un campo normal.
{"campo": null} encuentra también los documentos sin el campo porque en MongoDB «no existe» y «es nulo» se tratan casi igual; para distinguirlos está $exists.
Los operadores de comparación no mezclan tipos: {"edad": {"$gt": 18}} nunca encuentra un documento con "edad": "veinte". Es la consecuencia de no tener esquema: nadie impide guardar un texto donde otros guardan números.
MongoDB real tiene además índices, el pipeline de agregación ($match, $group, $lookup) y actualizaciones atómicas por documento; en este simulador, si una actualización falla en un documento, no se cambia ninguno.
Para ir más allá
- Añade
$elemMatchpara exigir que un mismo elemento de un array cumpla varias condiciones. - Programa un
aggregatecon las etapas$match,$group(con$sumy$avg) y$sort. - Haz que la notación de punto entre también en arrays de subdocumentos (
cursos.nota).