mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:43:49 +00:00
feat(elasticsearch): add hit document view and field type detection
This commit is contained in:
@@ -7,7 +7,7 @@ import TextContentSearchBar from "@/components/common/TextContentSearchBar.vue";
|
||||
import RedisJsonEditor from "@/components/redis/RedisJsonEditor.vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { formatJsonSource } from "@/lib/common/safeJsonFormat";
|
||||
import { formatJsonSource, parseJsonPreservingLargeNumbers, stringifyJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat";
|
||||
import { TEXT_CONTENT_SEARCH_MATCH_LIMIT, canFullHighlightTextContent, findTextContentMatches, nextTextContentSearchMatchIndex, renderTextContentMatchesHtml, textContentSearchStatus, type TextContentMatch } from "@/lib/common/textContentSearch";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -23,7 +23,10 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const responseView = ref<"raw" | "json">("json");
|
||||
type ResponseView = "raw" | "json" | "document";
|
||||
type ElasticsearchHit = { label: string; record: Record<string, unknown> };
|
||||
|
||||
const responseView = ref<ResponseView>("json");
|
||||
const responsePanelRef = ref<HTMLElement>();
|
||||
const rawPreRef = ref<HTMLPreElement>();
|
||||
const jsonEditorRef = ref<{ openSearch: () => boolean }>();
|
||||
@@ -41,6 +44,29 @@ const formattedBody = computed(() => {
|
||||
return { valid: false, text: "" };
|
||||
}
|
||||
});
|
||||
const documentHits = computed<ElasticsearchHit[]>(() => {
|
||||
try {
|
||||
const response = parseJsonPreservingLargeNumbers(props.body);
|
||||
const hits = response && typeof response === "object" && !Array.isArray(response) ? (response as Record<string, unknown>).hits : undefined;
|
||||
const hitDocuments = hits && typeof hits === "object" && !Array.isArray(hits) ? (hits as Record<string, unknown>).hits : undefined;
|
||||
if (!Array.isArray(hitDocuments)) return [];
|
||||
return hitDocuments.flatMap((hit, index) => {
|
||||
if (!hit || typeof hit !== "object" || Array.isArray(hit)) return [];
|
||||
const record = hit as Record<string, unknown>;
|
||||
const id = typeof record._id === "string" && record._id ? record._id : String(index + 1);
|
||||
// Keep the complete search hit, including optional fields such as
|
||||
// highlights and inner hits, without coercing Elasticsearch long values.
|
||||
return [{ label: id, record }];
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const selectedDocumentIndex = ref(0);
|
||||
const selectedDocument = computed(() => documentHits.value[selectedDocumentIndex.value]);
|
||||
// Keep raw records and stringify only the selected hit, so a large hit list
|
||||
// does not hold a formatted copy of every document in memory at once.
|
||||
const selectedDocumentText = computed(() => (selectedDocument.value ? stringifyJsonPreservingLargeNumbers(selectedDocument.value.record, 2) : ""));
|
||||
|
||||
const statusClass = computed(() => {
|
||||
if (props.status >= 500) return "border-destructive/40 bg-destructive/10 text-destructive";
|
||||
@@ -65,6 +91,7 @@ watch(
|
||||
() => props.body,
|
||||
() => {
|
||||
resetResponseSearch();
|
||||
selectedDocumentIndex.value = 0;
|
||||
responseView.value = formattedBody.value.valid ? "json" : "raw";
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -78,7 +105,7 @@ watch(responseSearchQuery, () => {
|
||||
|
||||
async function copyResponse() {
|
||||
try {
|
||||
await copyToClipboard(props.body);
|
||||
await copyToClipboard(responseView.value === "document" ? selectedDocumentText.value || props.body : props.body);
|
||||
toast(t("grid.copied"), 2000);
|
||||
} catch (error: any) {
|
||||
toast(t("grid.copyFailed", { message: error?.message || String(error) }), 5000);
|
||||
@@ -94,7 +121,7 @@ function handleResponsePanelPointerDown() {
|
||||
function focusSearch(): boolean {
|
||||
if (!responsePanelRef.value?.contains(document.activeElement)) return false;
|
||||
|
||||
if (responseView.value === "json") {
|
||||
if (responseView.value !== "raw") {
|
||||
void nextTick(() => jsonEditorRef.value?.openSearch());
|
||||
return true;
|
||||
}
|
||||
@@ -119,12 +146,17 @@ function closeResponseSearch() {
|
||||
resetResponseSearch(true);
|
||||
}
|
||||
|
||||
function switchResponseView(view: "raw" | "json") {
|
||||
function switchResponseView(view: ResponseView) {
|
||||
if (view === "document" && !documentHits.value.length) return;
|
||||
if (responseView.value === view) return;
|
||||
responseView.value = view;
|
||||
if (view !== "raw") resetResponseSearch();
|
||||
}
|
||||
|
||||
function selectDocument(index: number) {
|
||||
selectedDocumentIndex.value = index;
|
||||
}
|
||||
|
||||
function moveResponseSearchMatch(delta: -1 | 1) {
|
||||
const count = rawSearchMatches.value.length;
|
||||
if (count === 0) return;
|
||||
@@ -211,6 +243,16 @@ defineExpose({ focusSearch });
|
||||
>
|
||||
{{ t("redis.jsonView") }}
|
||||
</button>
|
||||
<button
|
||||
v-if="documentHits.length"
|
||||
type="button"
|
||||
class="h-6 rounded-[4px] px-2 text-xs transition-colors"
|
||||
:class="responseView === 'document' ? 'bg-background font-medium text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
||||
:aria-pressed="responseView === 'document'"
|
||||
@click="switchResponseView('document')"
|
||||
>
|
||||
{{ t("mongo.documentView") }}
|
||||
</button>
|
||||
<button v-if="canShowTable" type="button" class="h-6 rounded-[4px] px-2 text-xs transition-colors bg-background font-medium text-foreground shadow-sm" @click="emit('showTable')">
|
||||
{{ t("tabs.tableData") }}
|
||||
</button>
|
||||
@@ -226,6 +268,21 @@ defineExpose({ focusSearch });
|
||||
<div class="min-h-0 flex-1 overflow-hidden bg-background">
|
||||
<pre v-if="responseView === 'raw' && canHighlightRawSearch" ref="rawPreRef" class="m-0 h-full overflow-auto bg-transparent p-4 font-mono text-sm leading-6 whitespace-pre" v-html="highlightedRawResponse" />
|
||||
<pre v-else-if="responseView === 'raw'" ref="rawPreRef" class="m-0 h-full overflow-auto bg-transparent p-4 font-mono text-sm leading-6 whitespace-pre">{{ body }}</pre>
|
||||
<div v-else-if="responseView === 'document'" class="flex h-full min-h-0">
|
||||
<aside class="w-52 shrink-0 overflow-y-auto border-r bg-muted/10 p-1.5">
|
||||
<button
|
||||
v-for="(document, index) in documentHits"
|
||||
:key="`${document.label}:${index}`"
|
||||
type="button"
|
||||
class="flex w-full items-center rounded px-2 py-1.5 text-left font-mono text-xs transition-colors"
|
||||
:class="selectedDocumentIndex === index ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent/60 hover:text-foreground'"
|
||||
@click="selectDocument(index)"
|
||||
>
|
||||
<span class="truncate">{{ document.label }}</span>
|
||||
</button>
|
||||
</aside>
|
||||
<RedisJsonEditor v-if="selectedDocument" ref="jsonEditorRef" :model-value="selectedDocumentText" read-only class="min-h-0 min-w-0 flex-1" />
|
||||
</div>
|
||||
<RedisJsonEditor v-else-if="formattedBody.valid" ref="jsonEditorRef" :model-value="formattedBody.text" read-only class="min-h-0 flex-1" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+38
@@ -125,6 +125,44 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("ElasticsearchJsonResponsePanel search", () => {
|
||||
it("shows individual Elasticsearch search hits as complete JSON documents", async () => {
|
||||
const body = `{
|
||||
"hits": {
|
||||
"hits": [
|
||||
{
|
||||
"_id": "document-1",
|
||||
"_index": "products",
|
||||
"_score": 1,
|
||||
"_version": 4,
|
||||
"_source": { "name": "Ada", "accountId": 9007199254740993, "tags": ["admin"] },
|
||||
"highlight": { "name": ["<em>Ada</em>"] }
|
||||
},
|
||||
{ "_id": "document-2", "_source": { "name": "Grace" } }
|
||||
]
|
||||
}
|
||||
}`;
|
||||
const { panel, panelElement } = await mountResponsePanel(body, false);
|
||||
const documentButton = [...root!.querySelectorAll<HTMLButtonElement>("button")].find((button) => button.textContent === "mongo.documentView");
|
||||
|
||||
expect(documentButton).toBeDefined();
|
||||
documentButton!.click();
|
||||
await flushUi();
|
||||
expect(root!.querySelector("[data-redis-json-editor-stub]")?.textContent).toContain('"_id": "document-1"');
|
||||
expect(root!.querySelector("[data-redis-json-editor-stub]")?.textContent).toContain('"_version": 4');
|
||||
expect(root!.querySelector("[data-redis-json-editor-stub]")?.textContent).toContain('"highlight": {');
|
||||
expect(root!.querySelector("[data-redis-json-editor-stub]")?.textContent).toContain('"accountId": 9007199254740993');
|
||||
expect(root!.querySelector("[data-redis-json-editor-stub]")?.textContent).toContain('"tags": [');
|
||||
|
||||
[...root!.querySelectorAll<HTMLButtonElement>("aside button")].find((button) => button.textContent === "document-2")!.click();
|
||||
await flushUi();
|
||||
expect(root!.querySelector("[data-redis-json-editor-stub]")?.textContent).toContain('"_id": "document-2"');
|
||||
|
||||
panelElement.focus();
|
||||
expect(panel.focusSearch()).toBe(true);
|
||||
await flushUi();
|
||||
expect(mocks.openJsonSearch).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("uses the read-only Redis JSON editor for formatted JSON responses", async () => {
|
||||
const { panel, panelElement } = await mountResponsePanel('{"name":"Ada"}', false);
|
||||
const editor = root!.querySelector<HTMLElement>("[data-redis-json-editor-stub]");
|
||||
|
||||
@@ -287,6 +287,16 @@ const documentFilterFieldSearch = ref<Record<string, string>>({});
|
||||
const documentFilterRules = ref<DocumentFilterRule[]>([]);
|
||||
const appliedDocumentFilter = ref<Record<string, unknown> | null>(null);
|
||||
const elasticsearchMappingFields = ref<ColumnInfo[]>([]);
|
||||
function elasticsearchGridColumnTypesFor(columns: readonly string[]): string[] {
|
||||
const mappingTypes = elasticsearchFieldTypes.value;
|
||||
return columns.map((column) => {
|
||||
// Elasticsearch metadata fields are not part of an index mapping, but they
|
||||
// are textual identifiers in the document grid just like mapped keywords.
|
||||
if (column === "_id" || column === "_routing" || column === "_type") return "keyword";
|
||||
return mappingTypes.get(column) ?? "";
|
||||
});
|
||||
}
|
||||
const elasticsearchGridColumnTypes = computed(() => elasticsearchGridColumnTypesFor(lastGridColumns.value));
|
||||
const dynamodbTableDescription = ref<DynamoDbTableDescription | null>(null);
|
||||
const dynamodbIndexName = ref("__table__");
|
||||
const dynamodbPageCursors = ref<Array<string | undefined>>([undefined]);
|
||||
@@ -419,10 +429,11 @@ function commitLoadedDocuments(nextDocuments: JsonRecord[], nextCopyDocuments: J
|
||||
|
||||
const gridResult = computed<QueryResult>(() => {
|
||||
const docs = documents.value;
|
||||
const columnTypes = documentStoreProvider.value.kind === "elasticsearch" ? elasticsearchGridColumnTypes.value : lastGridColumnTypes.value;
|
||||
if (!docs.length) {
|
||||
return {
|
||||
columns: lastGridColumns.value,
|
||||
column_types: lastGridColumnTypes.value,
|
||||
column_types: columnTypes,
|
||||
rows: [],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
@@ -432,7 +443,7 @@ const gridResult = computed<QueryResult>(() => {
|
||||
|
||||
return {
|
||||
columns: lastGridColumns.value,
|
||||
column_types: lastGridColumnTypes.value,
|
||||
column_types: columnTypes,
|
||||
rows: gridRows.value,
|
||||
mongo_documents: docs,
|
||||
mongo_copy_documents: copyDocuments.value,
|
||||
@@ -513,7 +524,7 @@ async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExporte
|
||||
|
||||
const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null);
|
||||
if (result.columns.length === 0) result.columns = gridResult.value.columns;
|
||||
result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : undefined;
|
||||
result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : kind === "elasticsearch" ? elasticsearchGridColumnTypesFor(result.columns) : undefined;
|
||||
result.affected_rows = exportedDocuments.length;
|
||||
result.truncated = (kind === "dynamodb" || kind === "elasticsearch") && !!cursor && exportedDocuments.length >= rowLimit;
|
||||
result.has_more = result.truncated;
|
||||
@@ -879,7 +890,7 @@ async function applyDocumentStructuredFilters() {
|
||||
async function loadElasticsearchMappingFields() {
|
||||
if (documentStoreProvider.value.kind !== "elasticsearch") return;
|
||||
try {
|
||||
elasticsearchMappingFields.value = await api.getColumns(props.connectionId, props.database, "", props.collection);
|
||||
elasticsearchMappingFields.value = (await api.getColumns(props.connectionId, props.database, "", props.collection)) ?? [];
|
||||
} catch {
|
||||
elasticsearchMappingFields.value = [];
|
||||
}
|
||||
|
||||
+27
@@ -398,6 +398,33 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("DocumentBrowser Elasticsearch field search", () => {
|
||||
it("passes Elasticsearch mapping types through to the table grid", async () => {
|
||||
app?.unmount();
|
||||
backend.getColumns.mockResolvedValue([
|
||||
{ name: "profile", data_type: "object" },
|
||||
{ name: "profile.name", data_type: "keyword" },
|
||||
{ name: "title", data_type: "text" },
|
||||
]);
|
||||
backend.documentFindDocuments.mockResolvedValue({
|
||||
documents: [{ _id: "document-1", title: "Example", profile: { name: "Ada" } }],
|
||||
raw_documents: [],
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
});
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "connection-1",
|
||||
database: "",
|
||||
collection: "orders",
|
||||
databaseType: "elasticsearch",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
const types = JSON.parse(root!.querySelector<HTMLElement>("[data-testid='data-grid']")!.dataset.resultColumnTypes ?? "[]");
|
||||
|
||||
expect(types).toEqual(["keyword", "text", "object"]);
|
||||
});
|
||||
|
||||
it("migrates hidden columns and passes a stable index layout scope without changing the query result", async () => {
|
||||
app?.unmount();
|
||||
const legacyScopeKey = documentGridColumnVisibilityScopeKey({
|
||||
|
||||
@@ -25,6 +25,11 @@ describe("data grid type visual kind", () => {
|
||||
["UUID", "identifier"],
|
||||
["BYTEA", "binary"],
|
||||
["SDO_GEOMETRY", "spatial"],
|
||||
["keyword", "string"],
|
||||
["unsigned_long", "integer"],
|
||||
["scaled_float", "numeric"],
|
||||
["date_nanos", "temporal"],
|
||||
["nested", "structured"],
|
||||
["inet", "unknown"],
|
||||
])("maps %s to %s", (dataType, expected) => {
|
||||
expect(resolveDataGridTypeVisualKind(dataType)).toBe(expected);
|
||||
@@ -40,6 +45,10 @@ describe("data grid type visual kind", () => {
|
||||
["rowversion", "sqlserver", "binary"],
|
||||
["bit(8)", "postgres", "binary"],
|
||||
["bit varying(8)", "postgres", "binary"],
|
||||
["long", "elasticsearch", "integer"],
|
||||
["long", "easysearch", "integer"],
|
||||
["byte", "elasticsearch", "integer"],
|
||||
["short", "elasticsearch", "integer"],
|
||||
] as const)("maps %s for %s to %s", (dataType, databaseType, expected) => {
|
||||
expect(resolveDataGridTypeVisualKind(dataType, databaseType)).toBe(expected);
|
||||
});
|
||||
@@ -49,6 +58,10 @@ describe("data grid type visual kind", () => {
|
||||
expect(resolveDataGridTypeVisualKind("timestamp", "postgres")).toBe("temporal");
|
||||
expect(resolveDataGridTypeVisualKind("bit")).toBe("boolean");
|
||||
expect(resolveDataGridTypeVisualKind("bit", "sqlserver")).toBe("boolean");
|
||||
expect(resolveDataGridTypeVisualKind("long")).toBe("string");
|
||||
expect(resolveDataGridTypeVisualKind("long", "oracle")).toBe("string");
|
||||
expect(resolveDataGridTypeVisualKind("byte")).toBe("unknown");
|
||||
expect(resolveDataGridTypeVisualKind("short")).toBe("unknown");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
|
||||
|
||||
/**
|
||||
* Resolve the data type to display in a data-grid column header.
|
||||
@@ -148,6 +148,9 @@ const NUMERIC_COLUMN_TYPE_BASES = new Set([
|
||||
"smallmoneyn",
|
||||
"binary_float",
|
||||
"binary_double",
|
||||
// Elasticsearch numeric mapping types.
|
||||
"half_float",
|
||||
"scaled_float",
|
||||
]);
|
||||
|
||||
export function isNumericColumnType(dataType: string | undefined): boolean {
|
||||
@@ -194,8 +197,16 @@ const INTEGER_COLUMN_TYPE_BASES = new Set([
|
||||
"uint128",
|
||||
"uint256",
|
||||
"year",
|
||||
// Elasticsearch integer mapping types with unambiguous names.
|
||||
"unsigned_long",
|
||||
"token_count",
|
||||
]);
|
||||
|
||||
// Elasticsearch integer mapping types whose names collide with other
|
||||
// databases' non-integer types (Oracle LONG is a legacy text type, Informix
|
||||
// BYTE is binary), so they only apply to Elasticsearch-compatible databases.
|
||||
const ELASTICSEARCH_ONLY_INTEGER_COLUMN_TYPE_BASES = new Set(["byte", "short", "long"]);
|
||||
|
||||
const STRING_COLUMN_TYPE_BASES = new Set([
|
||||
"varchar",
|
||||
"varchar2",
|
||||
@@ -221,11 +232,20 @@ const STRING_COLUMN_TYPE_BASES = new Set([
|
||||
"character varying",
|
||||
"national character",
|
||||
"national character varying",
|
||||
// Elasticsearch text-like mapping types.
|
||||
"keyword",
|
||||
"constant_keyword",
|
||||
"wildcard",
|
||||
"match_only_text",
|
||||
"search_as_you_type",
|
||||
"completion",
|
||||
"ip",
|
||||
"version",
|
||||
]);
|
||||
|
||||
const BOOLEAN_COLUMN_TYPE_BASES = new Set(["bool", "boolean", "bit"]);
|
||||
const TEMPORAL_COLUMN_TYPE_BASES = new Set(["date", "date32", "daten", "time", "time64", "timen", "timetz", "datetime", "datetime2", "datetime4", "datetime64", "datetimen", "datetimeoffset", "datetimeoffsetn", "smalldatetime", "timestamp", "timestampdty", "timestamptz", "interval"]);
|
||||
const STRUCTURED_COLUMN_TYPE_BASES = new Set(["json", "jsonb", "jsonpath", "xml", "xmltype", "array", "map", "tuple", "struct", "row", "object", "document", "variant"]);
|
||||
const TEMPORAL_COLUMN_TYPE_BASES = new Set(["date", "date_nanos", "date32", "daten", "time", "time64", "timen", "timetz", "datetime", "datetime2", "datetime4", "datetime64", "datetimen", "datetimeoffset", "datetimeoffsetn", "smalldatetime", "timestamp", "timestampdty", "timestamptz", "interval"]);
|
||||
const STRUCTURED_COLUMN_TYPE_BASES = new Set(["json", "jsonb", "jsonpath", "xml", "xmltype", "array", "map", "tuple", "struct", "row", "object", "nested", "flattened", "document", "variant"]);
|
||||
const IDENTIFIER_COLUMN_TYPE_BASES = new Set(["uuid", "uniqueidentifier", "rowid", "urowid"]);
|
||||
const BINARY_COLUMN_TYPE_BASES = new Set(["bytea", "blob", "tinyblob", "mediumblob", "longblob", "binary", "varbinary", "image", "raw", "long raw", "bfile"]);
|
||||
const SPATIAL_COLUMN_TYPE_BASES = new Set(["geometry", "geography", "sdo_geometry", "point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon", "geometrycollection"]);
|
||||
@@ -280,6 +300,7 @@ export function resolveDataGridTypeVisualKind(dataType: string | undefined, data
|
||||
if (array) return "structured";
|
||||
if (databaseType === "sqlserver" && (base === "timestamp" || base === "rowversion")) return "binary";
|
||||
if (databaseType === "postgres" && (base === "bit" || base === "bit varying")) return "binary";
|
||||
if (isElasticsearchCompatibleDatabaseType(databaseType) && ELASTICSEARCH_ONLY_INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer";
|
||||
if (INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer";
|
||||
if (isNumericColumnType(dataType)) return "numeric";
|
||||
if (BOOLEAN_COLUMN_TYPE_BASES.has(base)) return "boolean";
|
||||
|
||||
@@ -627,16 +627,22 @@ fn collect_mapping_columns(
|
||||
for (name, definition) in properties {
|
||||
let field_name = if prefix.is_empty() { name.clone() } else { format!("{prefix}.{name}") };
|
||||
let field_type = definition.get("type").and_then(Value::as_str);
|
||||
let children = definition.get("properties").and_then(Value::as_object);
|
||||
|
||||
if let Some(data_type) = field_type {
|
||||
push_mapping_column(&field_name, data_type, seen, columns);
|
||||
} else if children.is_some() {
|
||||
// Elasticsearch infers `object` for a field with `properties` when
|
||||
// the mapping omits an explicit type. Return the parent too because
|
||||
// the document grid renders top-level object values as one column.
|
||||
push_mapping_column(&field_name, "object", seen, columns);
|
||||
}
|
||||
|
||||
if let Some(fields) = definition.get("fields").and_then(Value::as_object) {
|
||||
collect_mapping_columns(&field_name, fields, seen, columns);
|
||||
}
|
||||
|
||||
if let Some(children) = definition.get("properties").and_then(Value::as_object) {
|
||||
if let Some(children) = children {
|
||||
collect_mapping_columns(&field_name, children, seen, columns);
|
||||
}
|
||||
}
|
||||
@@ -2996,6 +3002,25 @@ mod tests {
|
||||
assert_eq!(names, vec!["ngx-log-1".to_string(), "ngx-log-2".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_columns_include_implicit_object_parents() {
|
||||
let mapping = json!({
|
||||
"profile": {
|
||||
"properties": {
|
||||
"name": { "type": "keyword" }
|
||||
}
|
||||
}
|
||||
});
|
||||
let properties = mapping.as_object().unwrap();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut columns = Vec::new();
|
||||
|
||||
super::collect_mapping_columns("", properties, &mut seen, &mut columns);
|
||||
|
||||
assert!(columns.iter().any(|column| column.name == "profile" && column.data_type == "object"));
|
||||
assert!(columns.iter().any(|column| column.name == "profile.name" && column.data_type == "keyword"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connectivity_check_disabled_parses_bool_and_string() {
|
||||
use super::elasticsearch_connectivity_check_disabled as disabled;
|
||||
|
||||
Reference in New Issue
Block a user