Skip to content

Guia de uso aprofundado de NoSQL — MongoDB / Redis / Elasticsearch

O SkylerX trata NoSQL como cidadão de primeira classe: mesma árvore de metadados, mesmo gerenciamento de conexões, mesma IA — mas o transporte usa o canal paralelo (executeCommand) — detalhes em ARCHITECTURE. Este documento descreve por banco as capacidades de UI e os ops + parâmetros realmente expostos no driver.

Visão geral — canal paralelo e canal SQL

DataClient expõe duas entradas independentes:

CanalEntradaAplica a
SQLconnections.execute(sql)MySQL / PostgreSQL / Oracle / ...
Commandconnections.executeCommand({ op, args, context, maxRows? })MongoDB / Redis / Elasticsearch

execute() em drivers NoSQL lança SQL_CHANNEL_UNSUPPORTED:

ts
// packages/core-driver/src/dialects/mongo.ts
async execute(): Promise<QueryResult> {
  throw new Error('SQL_CHANNEL_UNSUPPORTED: MongoDB não suporta SQL; use executeCommand')
}

executeCommand é o ponto de entrada real; cada driver define seu dicionário de ops. As próximas seções detalham esse dicionário.

Convenções:

  • context carrega o alvo (Mongo database / collection, Redis dbIndex, ES collection = index).
  • args é o objeto / array de parâmetros do op (objeto em Mongo / ES, array posicional em Redis).
  • maxRows só faz sentido em ops de leitura que retornam coleção; o driver usa limit/size + 1 para detectar truncated.
  • Retorno: CommandResult { data, executionTimeMs, affected?, truncated? }.

MongoDB

Árvore

Connection
└── Database (vários)
    └── Group "Collections" (count)
        └── Collection (kind = Table, reusa nó de tabela SQL)

No driver:

  • listDatabases chama admin().listDatabases().
  • databaseGroups usa listCollections({}, { nameOnly: true }) para preencher count.
  • listCollections ordenadas, viram kind: Table; front renderiza MongoPane quando connection.dialect === 'mongodb'.

Clicar no nó da Collection abre o componente; topo com 3 áreas:

  1. Breadcrumb database · collection, ao lado de refresh / commit / undo.
  2. Filter JSON (textarea) + limit / skip + alternância tabela / JSON.
  3. Resultado — colunas são união de chaves no primeiro nível; ou JSON cru.

O botão de execução chama find:

ts
await client.connections.executeCommand(conn.id, {
  op: 'find',
  args: {
    filter,
    options: { limit, skip },
  },
  context: { database, collection },
  maxRows: 500,
})

Cabeçalhos via união dinâmica das chaves nas linhas, ideal para coleções schemaless. Quando _id é string 24-hex, renderiza como ObjectId("...") (lembrete: por baixo é BSON ObjectId; IPC serializa como string).

Grade editável → updateOne (dot-path)

Duplo clique em célula não-_id entra em edição (_id proibido). Digite JSON válido, Enter confirma. Células dirty destacadas; topo "Commit (N)" → updateOne linha a linha.

Diff em diffToOps():

  • Nenhum lado é plain object → $set no campo todo (não desce em array para não bagunçar índices).
  • Ambos plain objects → união de chaves recursiva; só novo → $set; só antigo → $unset; igual → skip.
  • Path em dot-path, ex.: addr.city = '...'.

Request final:

ts
{
  op: 'updateOne',
  args: {
    filter: { _id: { $oid: '65f1...' } },
    update: { $set: { 'addr.city': 'BJ' }, $unset: { 'addr.zip': true } },
    options: {},
  },
  context: { database, collection },
}

Wrap automático de ObjectId ($oid marker)

ObjectId perde tipo no IPC (vira string). Convenção bidirecional:

  • UI ao reenviar: wrapOidStrings() envelopa strings 24-hex em { $oid: 'hex' }.
  • Driver ao receber: normalizeIds() converte strings 24-hex sob chave _id em new ObjectId(hex).

O driver é conservador: só converte sob chave _id; não toca em outras. Motivo nos comentários: evita converter strings de hash que coincidem com formato. Para consultar por userId / refId que são ObjectIds, use { $oid: '...' } ou EJSON completo.

Operadores dentro de _id também são tratados:

jsonc
{ "_id": "65f1aa..."                                      } // → ObjectId
{ "_id": { "$in": ["65f1aa...", "65f2bb..."]              }} // array
{ "_id": { "$eq": "65f1aa...", "$exists": true            }} // operadores
{ "$or": [{ "_id": "65f1aa..." }, { "name": "x" }]         } // aninhado

Aggregation pipeline (MongoAggregationDialog.vue)

Cards de stages à esquerda (mover / remover); resultado à direita. Cada stage é uma textarea JSON. STAGE_TEMPLATES com 10 templates de uso comum:

$match · $project · $group · $sort · $limit · $skip · $unwind · $lookup · $addFields · $count

Execução: monta { [stage.op]: JSON.parse(stage.json) } em ordem:

ts
{
  op: 'aggregate',
  args: {
    pipeline,
    options: { allowDiskUse: true, maxTimeMS: 30000 },
  },
  context: { database, collection },
}

Qualquer stage com JSON inválido → erro no pipeline inteiro. Top limit (input mini 1-1000) renderizado JSON full. details tem "Ver pipeline JSON" para copiar para mongosh.

Metadados da collection (MongoCollectionInfoDialog.vue)

Dois tabs:

Estatísticas (collStats): count / size / avgObjSize / storageSize / nindexes / totalIndexSize em unidades legíveis.

ÍndiceslistIndexes + tabela (name / keys / unique / sparse / ttl / size) + form de novo índice:

  • Linhas para campos; direção 1 / -1 / text / 2dsphere.
  • name / unique / sparse / expireAfterSeconds.
  • createIndex por trás: { key: { field1: 1, field2: -1 }, unique?, sparse?, expireAfterSeconds? }.

dropIndex para remover; UI bloqueia _id_ (índice default).

Ops suportados no driver

mongo.ts switch real:

Categoriaopargs obrigatóriosDescrição
Readfindfilter, options?Cursor toArray; maxRows → limit+1 para truncated
ReadfindOnefilter, options?Single doc
Readaggregatepipeline, options?Pipeline, maxRows idem
ReadcountDocumentsfilter, options?
Readdistinctfield, filter?, options?
WriteinsertOnedocument, options?affected = acknowledged ? 1 : 0
WriteinsertManydocuments, options?affected = insertedCount
WriteupdateOne / updateManyfilter, update, options?affected = modifiedCount
WritereplaceOnefilter, document, options?
WritedeleteOne / deleteManyfilter, options?affected = deletedCount
DBrunCommandargs inteiro como command em db.command()escape hatch
DBlistCollectionsfilter?, options?
DBcreateCollectionname, options?
DBdropCollectionname
IndexcollStats / listIndexes / createIndex / dropIndexver MongoCollectionInfoDialogpassa por runCommand

Ops fora da tabela → UNKNOWN_OP. Para adicionar, edite o switch em mongo.ts; não use API arbitrária.


Redis

Árvore

Connection
└── Database  db0..db15 (16 lógicos fixos, count via INFO keyspace)
    └── Group "Strings / Hashes / Lists / Sets / Sorted Sets / Streams"
        └── Key (SCAN amostral, max 200)

listDatabases usa INFO keyspace para todos os 16; bancos vazios sem count.

listTypeGroups analisa DBSIZE: <= 100 000 faz SCAN integral + pipeline TYPE com count exato; bancos maiores apenas mostram grupos sem contagem.

sampleKeysByType ao escolher grupo faz SCAN + pipeline TYPE com filtro, amostragem máxima SCAN_SAMPLE_LIMIT = 200, budget de varredura ~ROUND_CAP × COUNT = 50 × 200 = 10 000 keys. Restante mostra ... (mais, use SCAN) direcionando ao RedisSearchDialog.

Key browser (RedisPane.vue)

Esquerda: lista SCAN + MATCH. Direita: view conforme TYPE da key. Rodapé "Carregar mais" continua o cursor SCAN até retornar 0.

Carregamento:

  1. SCAN <cursor> MATCH <match> COUNT 500[nextCursor, batch].
  2. Novos keys → chunks (TYPE_PIPELINE_CHUNK = 200) → TYPE em paralelo.
  3. Append em keys.value, avança cursor.

Ordenação por name / type / ttl com desc/asc; TTL escondido por default; o botão "TTL" puxa em batch (TTL por key, chunks de 100). Multi-seleção permite EXPIRE / PERSIST / UNLINK em batch.

Render por TYPE

Driver executeCommand passa direto a ioredis.call(op, ...args); a UI envia comandos Redis nativos. Ao selecionar:

TYPEPequeno (≤ PAGE_SIZE = 100)Grande (paginado)
stringGET key
hashHGETALL keyHSCAN key cursor COUNT 100
listLRANGE key 0 LIST_PAGE-1 (LIST_PAGE = 200)LRANGE paginado, com LLEN
setSMEMBERS keySSCAN key cursor COUNT 100
zsetZRANGE key 0 -1 WITHSCORESZSCAN key cursor COUNT 100
streamXRANGE key - + COUNT 50

Entries de stream [id, [f1, v1, f2, v2, ...]] viram { id, fields: [[k, v], ...] }.

Views adicionais (mesmo TYPE base)

HyperLogLog / Bitmap usam string; Geo usa zset — TYPE não distingue, então a UI tem toggle manual:

  • HLL (string) → PFCOUNT key, erro ≈ 0.81%.
  • Bitmap (string) → BITCOUNT key (total) + range BITCOUNT key start end + bit GETBIT key offset.
  • Geo (zset) → ZRANGE key 0 -1 para membros + GEOPOS key m1 m2 ... em uma só chamada. Membros não-geo retornam nil = null.

Tipo errado (ex.: string normal como HLL) → WRONGTYPE no banner.

Edição inline

string / hash / list / set / zset suportam — botão "Editar" no topo; UI mantém draft, no salvar gera comandos mínimos:

  • string → SET key value
  • hash → HDEL key f1 f2 ... + HSET key f1 v1 f2 v2 ...
  • list → LSET key i v só onde mudou
  • set → SADD key m1 m2 ... e SREM key m1 m2 ...
  • zset → ZREM key m1 m2 ... e ZADD key s1 m1 s2 m2 ...

stream sem edição inline (semântica pesada).

Novo key (RedisNewKeyDialog.vue)

Cinco tipos visuais:

TipoComandoInput UI
StringSET key valuetextarea
HashHSET key f1 v1 ...linhas field/value
ListRPUSH key v1 v2 ...textarea, uma linha por item
SetSADD key m1 m2 ...textarea, dedup auto
Sorted SetZADD key s1 m1 s2 m2 ...linhas <score> <member>

TTL opcional; > 0 → EXPIRE key ttl. Pre-check com EXISTS key; já existe → rejeita (sem overwrite). stream fora — XADD requer id + field/value, mais fácil no input de comandos.

Input de comandos

Topo do RedisPane, segunda linha, editor de comando geral; tokens separados por whitespace:

ts
const op = tokens[0].toUpperCase()
const args = tokens.slice(1)
await client.connections.executeCommand(conn.id, {
  op,
  args,
  context: { dbIndex },
})

Vai direto ao executeCommandclient.call(op, ...args); todos os comandos Redis (incluindo DEBUG SLEEP, OBJECT ENCODING, CONFIG REWRITE). Atenção: sem escape de aspas — SET key "value with space" vira 4 tokens; valores com espaço use NewKey ou Lua.

Big keys (RedisBigKeysDialog.vue)

SCAN do banco inteiro + MEMORY USAGE por key (default SAMPLES 5, O(N)). Blocos de 20 keys em paralelo, serial entre blocos, botão "Parar". Top N desc (default 100), bucketing por prefixo : ("user / cache / session"), top 8 em barras horizontais para ver quem consome memória.

Centenas de milhares de keys → lento e CPU-intensivo, perceptível por outros clientes. Faça em horário tranquilo ou restrinja com MATCH.

Stream de comandos (RedisMonitorDialog.vue)

Trade-off-chave: MONITOR nativo é blocking e monopoliza a conexão, conflitando com nosso canal request-response. Solução: polling a cada N segundos (default 2000ms):

  • INFO statstotal_commands_processed / keyspace_hits / keyspace_misses / instantaneous_ops_per_sec
  • INFO clientsconnected_clients
  • INFO memoryused_memory

60 amostras em tabela rolante; hit ratio auto. Para detalhe de comando use redis-cli MONITOR (a UI deixa isso claro).

Painel de servidor (RedisServerInfoDialog.vue)

Sete tabs, cada um mapeia comandos administrativos:

TabComandosConteúdo
INFOINFOPor # Section, memória em unidades legíveis
Slow logSLOWLOG GET 128 + CONFIG GET/SET slowlog-log-slower-than + SLOWLOG RESETid / ts / μs / cmd / client
ClientesCLIENT LIST + CLIENT ID + CLIENT KILL ID <id>self com verde anti-kill
Stats de comandosINFO commandstatsOrdem usec_per_call desc
CONFIGCONFIG GET * + CONFIG SET k vEdição inline, filtro
ClusterCLUSTER INFO + CLUSTER NODESSlot bar (0-16383), hash por master; modo não cluster mostra motivo
SentinelSENTINEL MASTERSNão-sentinel mostra motivo

Auto-refresh 5s opcional por tab.

Lua / Functions (RedisScriptDialog.vue)

Dois tabs.

Lua:

  • Editor + KEYS / ARGV (space-separated).
  • ▶ EVALEVAL <script> <numKeys> KEYS... ARGV...
  • SCRIPT LOAD retorna sha, cache na UI; EVALSHA <sha>; SCRIPT FLUSH.
  • Save local em localStorage['skylerx.redis.lua.<connId>'], cross-session.

Functions (Redis 7+):

  • FUNCTION LIST WITHCODElibrary_name / engine / functions[].name / library_code.
  • Editor com library code → FUNCTION LOAD [REPLACE] <code>.
  • FUNCTION DELETE <lib>.
  • Click no nome puxa library_code para o editor.

Editor é textarea (não Monaco) — escolha deliberadamente leve; editor pesado é mais fácil no terminal.

Global SCAN (RedisSearchDialog.vue)

Cross 16 dbs:

  • Pattern + 16 checkboxes (default all), "Marcar / desmarcar tudo".
  • Itera selecionados, SCAN cursor MATCH ... COUNT 500; hits + TYPE / TTL em paralelo.
  • Hit > SCAN_PER_DB_LIMIT = 5000 por db → corta com toast.
  • Click em linha → pick(db, key); Workspace alterna para RedisPane correto via pendingKey.

Import / export (RedisImportExportDialog.vue)

JSON custom (sem RDB), para cross-instâncias:

json
[
  { "db": 0, "key": "...", "type": "string", "ttl": 3600, "value": "..." },
  { "db": 0, "key": "...", "type": "hash", "ttl": -1, "value": { "f": "v" } },
  { "db": 0, "key": "...", "type": "zset", "ttl": 0, "value": [{ "member": "a", "score": "1" }] },
  { "db": 0, "key": "...", "type": "stream", "ttl": -1, "value": [{ "id": "1-0", "fields": [["f","v"]] }] }
]

Export: SCAN MATCH no db atual, por key TYPE / TTL / dado serial (evita rajada de IPC); finaliza com client.files.saveText.

Import: parse JSON → restaura comando por type: string → SET, hash → HSET, list → RPUSH, set → SADD, zset → ZADD, stream → XADD por entry. Conflito skip (default) / overwrite (DEL antes). ttl > 0EXPIRE.

Limitação conhecida: stream sem consumer group; XINFO / XGROUP à parte.


Elasticsearch

Árvore

Connection
└── Index (flat, sem Database)
    └── Field (de getMapping properties)
  • listIndices via client.cat.indices({ format: 'json' }), filtra . (sistemas; desligável com extra.showSystemIndices = true).
  • listFields via client.indices.getMapping({ index }), mappings.properties, detail.dataType = prop.type (default object).

Painel de query (ElasticPane.vue)

  • Topo: breadcrumb (index) + Refresh + badge docs.count (chamada count independente).
  • Meio: textarea para Query DSL + op: search / count / getMapping.
  • Rodapé: Executar + toggle tabela / JSON.

Execução:

ts
await client.connections.executeCommand(conn.id, {
  op,                                  // 'search' | 'count' | 'getMapping'
  args: { index, body },               // body do textarea como JSON
  context: { collection: index },      // ambos preenchidos, driver tem needIndex()
  maxRows: 500,                        // só vale para search
})

getMapping sem body; count passa body como { query: ... }.

Tabela vs JSON

  • search: colunas = _id + união de campos hits.hits[*]._source; valor por cellOf(hit, col) (_idhit._id, demais → hit._source[col]).
  • Topo total: N · took: M ms do data.hits.total ({ value: N } ou number antigo) + executionTimeMs.
  • count / getMapping sem "linhas" → cai direto em JSON.
  • Toggle para raw JSON em qualquer op.
ts
case 'search': {
  const params = { index, ...body }
  let probeTruncated = false
  if (typeof maxRows === 'number' && body.size == null) {
    params.size = maxRows + 1            // probe extra
    probeTruncated = true
  }
  const res = await this.client.search(params)
  const data = unwrap(res)
  if (probeTruncated && data?.hits?.hits?.length > maxRows) {
    data.hits.hits = hits.slice(0, maxRows)
    return { data, executionTimeMs, truncated: true }
  }
  return { data, executionTimeMs }
}
  • size no DSL pelo usuário é respeitado.
  • Sem sizemaxRows + 1 para probe; hit > maxRows → corta e truncated: true.
  • Retorna estrutura ES original { hits: { hits, total } }, apenas hits.hits truncado.

Ops suportados

elasticsearch.ts:

CategoriaopargsMétodo
Doc readsearchindex?, body?client.search({ index, ...body })
Doc readgetindex?, idclient.get({ index, id })
Doc readcountindex?, query?client.count({ index, query })
Doc writeindexindex?, document, id?client.index({ index, document, id? }), affected = 1
Doc writeupdateindex?, id, doc?, body?client.update({ index, id, doc, ...body }), affected = 1
Doc writedeleteindex?, idclient.delete({ index, id }), affected = 1
Doc writebulkoperations[]client.bulk({ operations }), affected = items.length
Indexindices.create / indices.delete / indices.getMapping / indices.refreshpassa args para client.indices.<sub>
catcat.indices / cat.health / cat.nodespassa + default format: 'json'
Fallbackrawmethod, path, body?, querystring?client.transport.request(...), REST direto

needIndex() extrai de context.collection ou args.index; ausente → MISSING_INDEX.

unwrap(res) compatível com ES 8 (body direto) e v7 ({ body, statusCode, headers, warnings, meta }); UI não precisa se preocupar.


Contrato do canal paralelo (resumo)

Os 3 drivers diferem muito, mas o contrato para o front é estável:

ts
interface CommandRequest {
  op: string                   // dicionário do driver
  args?: unknown               // Mongo/ES = obj; Redis = array posicional
  context?: {                  // alvo
    database?: string          // Mongo
    collection?: string        // Mongo / ES (= index)
    dbIndex?: number           // Redis
  }
  maxRows?: number             // driver implementa truncado com limit+1
}

interface CommandResult {
  data: unknown
  executionTimeMs: number
  affected?: number            // rows afetadas em writes
  truncated?: boolean          // flag de truncamento
}

Independente de SQL: QueryResult só para SQL. Drivers NoSQL execute() lançam SQL_CHANNEL_UNSUPPORTED; com dialect = mongo/redis/elasticsearch, o front nem chama.


Limitações / trade-offs

ItemDescrição
Mongo 24-hex false-positiveStrings 24-hex que não são ObjectIds podem ser convertidas. Preço para corrigir "updateOne nunca hit".
Mongo ObjectId em campo não-_idDriver só converte _id automaticamente; userId / refId precisam de { $oid: 'hex' } ou EJSON manual via runCommand.
Redis MONITORBlocking; polling de INFO stats é o substituto. Detalhe por comando: redis-cli MONITOR.
Redis parser sem aspasRedisPane tokeniza por whitespace, sem escape. Valores com espaço use NewKey ou Lua.
Redis SCAN amostralCada grupo de tipo mostra max 200 keys, budget 10k. Maior: use global SCAN (RedisSearchDialog).
Redis tipos sem countDBSIZE > 100 000 → sem count para evitar SCAN lento na árvore.
Redis big keys MEMORY USAGEO(N), lento e CPU-pesado. Restrinja com MATCH ou rode fora de pico.
Redis import/export streamSem consumer group; XINFO / XGROUP à parte.
Redis new key sem streamXADD semântica pesada; use input de comandos / Lua.
ES SQL_xpack/sql não ANSI; sem canal SQL no momento; abra op: 'sql' se precisar.
ES size explícitoDSL com size é respeitado, sem +1 probe; sem flag truncated.
ES truncated só em searchcount / get / getMapping não têm coleção.
Deps NoSQLmongodb / ioredis / @elastic/elasticsearch são peerDeps opcionais, import lazy. Desktop já vem; backend self-hosted precisa pnpm add, senão connect/test lança "driver não instalado".

Apache License 2.0 · Wuhan Skyler Network Technology Co., Ltd.