Skip to content

feat(plugin-elasticsearch): add Elasticsearch driver plugin (#1529)#1751

Merged
datlechin merged 4 commits into
mainfrom
feat/elasticsearch-plugin
Jun 22, 2026
Merged

feat(plugin-elasticsearch): add Elasticsearch driver plugin (#1529)#1751
datlechin merged 4 commits into
mainfrom
feat/elasticsearch-plugin

Conversation

@datlechin

Copy link
Copy Markdown
Member

Closes #1529.

Adds Elasticsearch as a registry-only driver plugin (7.x/8.x), pure-Swift over the REST API with no C bridge.

What it does

  • Browse indices as tables; documents page into the data grid with _id/_index/_score meta columns. Nested object/nested fields flatten to dotted columns; arrays render as JSON cells.
  • Query DSL console (Kibana Dev Tools style): METHOD /path plus an optional JSON body. Search responses tabularize; _cat/array responses become a grid; everything else shows formatted JSON.
  • Filtering and sorting map to the Query DSL. Sorting a text field targets its .keyword sub-field; _id and other non-sortable fields are skipped so the query doesn't error.
  • Edit, insert, and delete documents (_update/_doc), keyed by _id.
  • Auth: HTTP Basic, API key, or none; TLS with an optional skip-verify for self-signed certs (8.x ships TLS on by default).
  • Deep paging past index.max_result_window (10k) uses search_after with a point-in-time.

Approach

DSL-first, matching how mature ES clients (Kibana, Elasticvue, Dejavu, EDQL) work. ES SQL (_sql) is read-only and blind to arrays/nested fields, so it is not the foundation. Built on the existing tagged-query dispatch pattern (like DynamoDB).

Included fixes

  • The data grid's raw filter dropped its text before reaching plugin drivers (asPluginFilterTuple forwarded value, but a raw filter stores its text in rawSQL). This broke raw filters on every NoSQL plugin, not just Elasticsearch. The Elasticsearch plugin maps the raw filter to a query_string full-text search.
  • The grid appends a SQL ORDER BY to the plugin query string when sorting; the plugin now parses and applies it as an ES sort.

Integration

DatabaseType.elasticsearch, new pbxproj bundle target, build-plugin.yml case, PluginMetadataRegistry seed (so it shows in the picker pre-install), icon imageset, docs page, CHANGELOG, version bump to 0.53.0 (the release this ships in).

Tests

Unit tests for the console parser, query builder + DSL, mapping flattener, statement generator, the appended-ORDER BY parsing, the __RAW__ mapping, and the asPluginFilterTuple raw-filter fix.

Notes

  • Registry-only, not bundled. scripts/install-plugin-dev.sh installs a local build for testing.
  • Verified end to end against Elasticsearch 8.13.4: browse, filter, sort, console, and document edits.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d2f4891fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if isTextField(column, fields: fields) {
return ["match": [column: value]]
}
return ["wildcard": [keywordField(column, fields: fields): ["value": "*\(escapeWildcard(value))*", "case_insensitive": true]]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate case-insensitive filters by server version

This plugin advertises Elasticsearch 7.x support, but keyword CONTAINS always emits case_insensitive; Elastic documents this wildcard parameter as added in 7.10.0, and the prefix branch below uses the same 7.10-only option, so 7.0-7.9 clusters reject these grid filters with a parsing error. Omit the option or gate it on serverVersion for pre-7.10 clusters.

Useful? React with 👍 / 👎.

Comment on lines +286 to +287
default:
return equalsClause(column: column, value: value, fields: fields)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle unsupported grid operators explicitly

When the filter UI sends operators already defined by TableFilter such as NOT CONTAINS, NOT IN, BETWEEN, REGEX, IS EMPTY, or IS NOT EMPTY, this falls through to equality instead of either translating or rejecting them. For example a numeric BETWEEN filter is passed as op == "BETWEEN" with value "10,20", which becomes a term query for that string rather than a range, so filtered browsing can return incorrect results without any error.

Useful? React with 👍 / 👎.

Comment on lines +142 to +143
private func encodePathComponent(_ value: String) -> String {
value.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? value

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape slashes in document IDs

urlPathAllowed leaves / unescaped, so an Elasticsearch _id such as tenant/123 is encoded into /index/_doc/tenant/123?refresh=true instead of a single path segment. In that case explicit inserts, updates, and deletes for otherwise valid document IDs fail because Elasticsearch sees the extra path component; use a path-component character set that excludes /.

Useful? React with 👍 / 👎.

Comment on lines +107 to +111

static func flattenSource(_ source: [String: Any]) -> [String: PluginCellValue] {
var result: [String: PluginCellValue] = [:]
flatten(value: source, prefix: "", into: &result)
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve mapped flattened objects when rendering rows

For mapped fields whose value is an object but whose mapping column is the parent field itself, such as Elasticsearch flattened fields, columns(forHits:) includes labels from the mapping but flattenSource only emits labels.foo keys and never stores a value for labels. Those columns therefore render as null even when the document contains data; keep the parent JSON value when the mapping expects the parent column, or pass mapping/type context into flattening.

Useful? React with 👍 / 👎.

@mintlify

mintlify Bot commented Jun 22, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jun 22, 2026, 12:40 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a344b9e7a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


var document: [String: Any] = [:]
for column in columns where !metaColumns.contains(column) {
guard let value = values[column], let text = value.asText, !text.isEmpty else { continue }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty strings on inserted documents

When a new row contains .text("") for a non-meta field, this guard treats it like no value and omits the key from the _doc body. Elasticsearch preserves empty strings in _source distinctly from missing fields, and the update path below does send empty strings, so inserting a document with an intentionally blank keyword/text field saves it as absent/null in TablePro. Only skip .null values, not empty text.

Useful? React with 👍 / 👎.

Comment on lines +188 to +190
if let json = response.json as? [String: Any], json["hits"] is [String: Any] {
let hits = extractHits(response)
return renderHits(hits, mappingColumns: [], fields: [:], startTime: startTime)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't drop aggregation results from console searches

In the console, any response with a top-level hits object is forced through renderHits. Elasticsearch search requests with aggs include both hits and aggregations; for common size: 0 aggregation queries this returns an empty grid and discards the aggregation payload entirely, despite the plugin offering an aggs completion. Render raw JSON or otherwise preserve the aggregation/suggest sections when they are present.

Useful? React with 👍 / 👎.

Comment on lines +161 to +164
if let data = text.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(with: data),
parsed is [Any] || parsed is [String: Any] {
return parsed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep JSON-looking strings as scalar field values

This coerces any valid JSON object or array text into a structured value regardless of the mapped column type. If a scalar text, keyword, date, or ip field contains a literal string like { "event": "login" }, saving an edit sends an object/array instead of the original string and Elasticsearch will either reject it as a type conflict or change the stored shape. Gate JSON parsing to structured field types instead of applying it to every non-numeric/non-boolean column.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit bcfb71d into main Jun 22, 2026
2 checks passed
@datlechin datlechin deleted the feat/elasticsearch-plugin branch June 22, 2026 14:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a20db4c0d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +81 to +82
hidesPassword: true,
visibleWhen: FieldVisibilityRule(fieldId: "esAuthMethod", values: ["apiKey"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the password field visible for Basic auth

With the current ConnectionField+PasswordHiding helper, any authentication field marked hidesPassword hides the standard password control without checking visibleWhen; because this API-key field is always present in the Elasticsearch auth fields, the default Username & Password mode shows a username box but no password box, making Basic-auth Elasticsearch clusters impossible to configure from a new connection. The registry default declares the same field too, so both descriptors need the same treatment.

Useful? React with 👍 / 👎.

for: parsed, fields: fields, size: Self.deepPageBatchSize,
tiebreaker: true, searchAfter: searchAfter, caseInsensitive: supportsCaseInsensitiveSearch
)
body["pit"] = ["id": pit, "keep_alive": Self.pitKeepAlive]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the PIT id between deep-page requests

When browsing past max_result_window, Elasticsearch can return a new PIT id on each search response and expects the next search_after request to use the most recent id; this loop keeps sending the original pit value and also closes only that original id. On clusters that rotate PIT ids, deep paging can fail or leave the latest PIT context unclosed after the first batch.

Useful? React with 👍 / 👎.

return session
}

guard let url = URL(string: path, relativeTo: baseURL) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject network-path console URLs

For console input like GET //attacker.example/_search, URL(string:relativeTo:) treats the path as a network-path URL and sends the request to that host while still attaching this connection's Authorization header. The console should be constrained to the configured Elasticsearch base URL, e.g. by rejecting // paths or verifying the resolved host before building the request.

Useful? React with 👍 / 👎.

Comment on lines +299 to +301
case "IS NULL", "IS_NULL", "IS EMPTY", "IS_EMPTY":
return ["bool": [
"should": [mustNot(["exists": ["field": column]]), ["term": [keywordField(column, fields: fields): ""]]],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep NULL filters distinct from empty-string filters

The UI has separate IS NULL and IS EMPTY operators, but this branch makes IS NULL match present empty strings and makes IS NOT NULL exclude them; for keyword/text fields that legitimately store "", the grid counts and rows disagree with the selected operator, and for numeric/date fields the added term for an empty string can make Elasticsearch reject the filter. Only the empty operators should add the empty-string term.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Elasticsearch Support

1 participant