feat: add Apache Kafka driver - #93
Open
Siyet wants to merge 3 commits into
Open
Conversation
Introduces a KafkaDriver on top of `kafkajs` (pure JS, no native deps) so users can browse topics, consume and produce messages without leaving VS Code. The schema tree shows a single `cluster` keyspace with `topics` and `consumer groups` sections; topics expose their partitions as children and "Show Messages" / `CONSUME` render the last N records in the Result Panel. Query DSL parsed by `parseKafkaCommand`: LIST TOPICS | LIST GROUPS DESCRIBE <topic> CONSUME <topic> [N] [from-beginning] PRODUCE <topic> [key] <value> `PRODUCE` is blocked on read-only connections. SASL/PLAIN is wired via username/password; `options.saslMechanism` overrides the mechanism. The Host field accepts a comma-separated bootstrap broker list.
- subscribe({ topics: [topic] }) to use kafkajs 2.x shape and silence
the deprecation warning emitted on every consume.
- parseBrokerList falls back to localhost:<port> when the host string
trims/splits to zero non-empty parts (previously produced an empty
brokers array → opaque kafkajs error).
- normalizeHeaders handles multi-valued (array) header values and drops
keys whose values are all null/undefined.
- getTableInfo no longer reports partition count as the topic row
count; topics have no meaningful row count.
https://claude.ai/code/session_019u3fc2f7V6FDVUwZET3Vub
Siyet
commented
Apr 15, 2026
Siyet
left a comment
Owner
Author
There was a problem hiding this comment.
Fixes pushed
1bb3b99—consumer.subscribe({ topics: [topic] })uses the kafkajs 2.x signature; the old{ topic }shape logs a deprecation warning on every consume.1bb3b99—parseBrokerListfalls back tolocalhost:<port>when the trimmed/split broker list is empty (e.g." "or", ,"). Previously produced an empty brokers array → opaque kafkajs error.1bb3b99—normalizeHeadershandles multi-valued (array) header values and drops keys whose values are all null/undefined. kafkajsIHeadersallows arrays; the old path stringified them to"[object Object]".1bb3b99—getTableInfono longer reports partition count asrowCount; topics have no meaningful row count and reporting partitions is misleading downstream.
Issues to address
src/drivers/kafka.ts:258-295—consumeMessagescreates an ephemeralviewstor-<ts>-<rand>consumer group every call and never removes it. With many previews these groups accumulate on the broker until the cluster'soffsets.retention.minuteskicks in (7 days by default). Callthis.admin.deleteGroups([groupId])afterconsumer.disconnect()in a swallowed try/catch.src/drivers/kafka.ts:189-225—getSchemasurfaces internal topics (__consumer_offsets,__transaction_state,_schemas) alongside user topics with no way to collapse them. Filtert.name.startsWith('_')into a separate "Internal topics" group, or at minimum hide by default — the tree quickly becomes noisy on any real cluster.src/drivers/kafka.ts:247-256—getTableDataignoresoffsetandorderByfrom the driver interface. Result Panel pagination (next page / sort columns) silently re-fetches the most recent N messages every time. Either short-circuit pagination in the Result Panel for Kafka connections, or surface the limitation (e.g. a banner "Kafka topics are non-paginated — useCONSUME <topic> N from-beginningfor historical data").src/drivers/kafka.ts:156-177— PRODUCE is not routed through Safe Mode, yet it's a write-like operation.safeMode: 'block'/'warn'on a Kafka connection has no effect; users expecting the same guardrails as SQL writes will be surprised. Either wire Safe Mode or document that it is intentionally SQL-only.src/drivers/kafka.ts:266-289— the 5s timerresolve()callback doesn'tclearTimeoutor short-circuit furthereachMessageinvocations; the consumer keeps pushing intocollected(no-oped by the cap check) untildisconnect()returns. Minor, but setscollected.pushwork on the critical path after we've already returned to the caller.src/test/kafka.test.ts— no coverage for the CONSUME limit clamp (cap = Math.max(1, Math.min(limit, MAX_RESULT_ROWS))), e.g.CONSUME t 0andCONSUME t 99999999. Worth oneit(...)since the clamp is the only guard against a user-supplied limit blowing pastMAX_RESULT_ROWS.
CI note
E2E Tests job on 24483389082 failed in ~56s (Lint / Unit / Build / Audit / Changeset all pass). The failure appears unrelated to this PR — no e2e file references the Kafka driver, and the e2e suite doesn't import createDriver. Local vitest run src/test/e2e/sqlite.e2e.test.ts passes. Likely a testcontainers / Docker Hub flake on the runner; re-run should clear it. Logs were not accessible to this review.
Generated by Claude Code
If subscribe/run rejects, the existing code skipped the disconnect call and leaked a connected consumer plus its throwaway group on the broker. Wrap post-connect logic in try/finally and clear the 5s timeout on the reject path so it does not linger.
Siyet
commented
Apr 16, 2026
Siyet
left a comment
Owner
Author
There was a problem hiding this comment.
Fixes pushed
8519d0e— Always disconnect the Kafka consumer on error paths. The previous code ransubscribe()and the consume Promise outside any try/finally, so a broker-side failure would skipconsumer.disconnect()and leak a connected consumer plus its throwaway group (only cleaned up aftersessionTimeout). Also clears the 5s timer on therun().catch(reject)path so it doesn't linger after a reject.
Issues to address
src/commands/tableCommands.ts:50— Right-clicking a Kafka topic → Show Table Data opens the Result Panel withreadonlyderived fromisConnectionReadonly(id). For a non-readonly kafka connection, the UI exposes row-edit/insert/delete buttons whose_saveEdits→buildUpdateSqloutput gets rejected byparseKafkaCommandasUnknown Kafka command: UPDATE. Either forcereadonly = truewhenconfig.type === 'kafka', or hide the edit controls when the driver can't satisfy them.src/drivers/kafka.ts:38—this.readonlyMode = !!config.readonlyonly reflects the connection's own flag, not the folder-inherited readonly thatConnectionManager.isConnectionReadonly()computes. A connection inside a readonly folder that doesn't set its own flag will still run PRODUCE. Same pattern exists in other drivers, but it matters more here because PRODUCE is the driver's only write path and nothing above the driver gates it.src/drivers/kafka.ts:256—consumeMessagescreates a fresh throwaway consumer group on every call. ForgetTableData(tree → Show Table Data) this hides the pagination semantics: pages >1 just re-consume from latest/from-beginning again instead of continuing from the previous offset, so the Result Panel's pager is a no-op. Either accept a starting offset and seek, or wire the Result Panel to disable paging for kafka.src/drivers/kafka.ts:70—ping()callsthis.admin.listTopics()without checking whetheradminis defined. Afterdisconnect()setsadmin = undefined, a ping returnsfalsevia the catch block but the underlyingTypeErrorgets swallowed silently — fine for the current callers, worth a guard ifpingstarts being used for liveness after disconnect.src/test/kafka.test.ts— Coverage is solid forparseKafkaCommand/parseBrokerList/normalizeHeaders, but there's no test for the consumer lifecycle (cap reached, timeout, reject path). A fakeconsumerthat invokeseachMessageN times and one that rejectsrun()would catch regressions in the new try/finally and the timer/reject cleanup.
Generated by Claude Code
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
KafkaDriveron top ofkafkajs(pure JS, no native deps). Lazy-required insideconnect()so non-kafka users never pay.cluster→topics/consumer groups; each topic carries its partitions as children. Tree icon falls back to codiconbroadcast(no glyph in the viewstor icon font yet).parseKafkaCommand():LIST TOPICS,LIST GROUPS,DESCRIBE <topic>,CONSUME <topic> [N] [from-beginning],PRODUCE <topic> [key] <value>.PRODUCEis blocked on read-only connections.getTableData(topic)=CONSUME <topic>with up toMAX_RESULT_ROWSmessages.options.saslMechanismoverrides toscram-sha-256/scram-sha-512). Host accepts a comma-separated bootstrap broker list. SSL and Proxy/Advanced sections hidden for kafka.Closes #12
Assumptions
testcontainers, "consumer groups as a separate node" detail view, topic configs fromdescribeConfigs, message-browser JSON expansion polish, safe mode for PRODUCE outside readonly). Shipping the driver first lets us react to real usage — each of the remaining items slots in without changing the driver interface. The current minimum is enough to register Kafka connections, browse the cluster, and run round-trips.getTableData(topic)uses a throwaway consumer group.viewstor-<ts>-<rand>with a 5-second max wait andfromBeginning: false, so opening a topic shows "recent activity". Asking for history goes throughCONSUME <topic> N from-beginningexplicitly. This matches the issue's "consume last N messages" framing and keeps the UI fast on high-throughput topics.getDDL/getCompletions/getIndexedColumns/getTableStatistics. Topics aren't SQL and autocomplete / DDL / index hints don't have a natural mapping.driverContract.test.tsexplicitly asserts the empty optional set so future additions force an update here.list_connectionsbecause that tool is driver-agnostic, andexecute_queryaccepts the DSL strings as-is.build_chartover topic data works once a user opens a topic and pins the consume result — covered by existing MCP plumbing, no code change needed.package.nls.jsonif this ships.Follow-ups (not in this PR, tracked under #12)
testcontainers(confluentinc/cp-kafka).describeConfigs: retention, cleanup policy) underShow Table Info.valuecells.viewstor-kafkaglyph in the viewstor icon font.Manual test cases
docker run -p 9092:9092 apache/kafka:3.8.0(or any single-broker cluster). New Connection → type Apache Kafka → Hostlocalhost, Port9092. Test connection → green. Save, expand the tree. Expect:cluster→topicswith any pre-existing topics, each expandable to partitions.LIST TOPICS;→ result grid showstopic | partitions. RunCONSUME __consumer_offsets 10 from-beginning→ 10 rows withoffset,partition,key,value,timestamp,headers.PRODUCE demo hello "world"→produced 1 message to demo. ThenCONSUME demo from-beginning→ your message appears.PRODUCE demo hello world→ errorPRODUCE is not allowed on a read-only Kafka connection.CONSUMEstill works.broker1:9092, broker2:9092. Test connection. Expect: kafkajs connects through whichever broker responds first; same tree output.testResultbanner.DESCRIBE demo→ one row per partition withleader,replicas,isrJSON.SELECT 1in a kafka editor → errorUnknown Kafka command: SELECT. No broker traffic.Checklist
[Unreleased] / Addedentry with(#12)linkkafka.ts (kafkajs)added to the Drivers line plus a new Kafka-driver paragraph documenting the DSL, schema shape, and SASL wiringsrc/test/kafka.test.ts(24 tests):parseKafkaCommandverb + option handling,parseBrokerListwith/without port and with comma-separated lists,normalizeHeadersforUint8Array/null/string/numberKafkaDriverspec with emptyexpectedOptionalset;kafkajsmocked alongside the other native modulespackage.nls.jsonwhen the first non-English string lands