From 0172ee50be4e83899710e45b9a7beef2645295d5 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 13 May 2026 19:26:45 +0800 Subject: [PATCH] fix(neo4j): support table querying and editing --- src/components/grid/DataGrid.vue | 13 ++- src/components/objects/ObjectBrowser.vue | 12 ++- src/components/sidebar/TreeItem.vue | 35 ++++++-- src/composables/useDataGridActions.ts | 7 +- src/composables/useNavigationTargets.ts | 21 +++++ src/lib/dataGridSql.ts | 107 ++++++++++++++++++++++- src/lib/tableEditing.ts | 9 +- src/lib/tableSelectSql.ts | 29 +++++- tests/dataGridSql.test.ts | 51 +++++++++++ tests/tableEditing.test.ts | 7 ++ tests/tableSelectSql.test.ts | 27 ++++++ 11 files changed, 303 insertions(+), 15 deletions(-) diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index f39dac6e8e..5d745587c0 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -826,7 +826,7 @@ function currentWhereInput(): string | undefined { function currentOrderBy(): string | undefined { return ( orderByInput.value.trim() || - (sortCol.value ? `${quoteIdent(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined) + (sortCol.value ? `${queryColumnRef(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined) ); } @@ -1313,7 +1313,7 @@ type FilterMode = function contextFilterCondition(mode: FilterMode): string | null { if (!contextColumn.value) return null; - const column = quoteIdent(contextColumn.value); + const column = queryColumnRef(contextColumn.value); const value = contextCellValue.value; if (mode === "is-null") return `${column} IS NULL`; @@ -1355,6 +1355,7 @@ async function applyOrderBySearch() { databaseType: props.databaseType, schema: props.tableMeta.schema, tableName: props.tableMeta.tableName, + columns: props.tableMeta.columns.map((column) => column.name), primaryKeys: props.tableMeta.primaryKeys, orderBy: orderByClause, limit: pageSize.value, @@ -1379,10 +1380,11 @@ async function applyWhereFilter() { databaseType: props.databaseType, schema: props.tableMeta.schema, tableName: props.tableMeta.tableName, + columns: props.tableMeta.columns.map((column) => column.name), primaryKeys: props.tableMeta.primaryKeys, orderBy: orderByInput.value.trim() || - (sortCol.value ? `${quoteIdent(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined), + (sortCol.value ? `${queryColumnRef(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined), limit: pageSize.value, whereInput: whereFilterInput.value.trim() || undefined, includeRowId: tableUsesSyntheticRowId.value, @@ -1408,6 +1410,11 @@ function quoteIdent(name: string): string { return quoteTableIdentifier(props.databaseType, name); } +function queryColumnRef(name: string): string { + const quoted = quoteIdent(name); + return props.databaseType === "neo4j" ? `n.${quoted}` : quoted; +} + function escapeVal(value: CellValue): string { return formatGridSqlLiteral(value); } diff --git a/src/components/objects/ObjectBrowser.vue b/src/components/objects/ObjectBrowser.vue index 27f41c0c2e..9e6a0ac2fe 100644 --- a/src/components/objects/ObjectBrowser.vue +++ b/src/components/objects/ObjectBrowser.vue @@ -31,7 +31,7 @@ import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import * as api from "@/lib/api"; import type { ConnectionConfig, ObjectInfo, ObjectSourceKind } from "@/types/database"; import { isSchemaAware } from "@/lib/databaseCapabilities"; -import { qualifiedTableName } from "@/lib/tableSelectSql"; +import { buildTableSelectSql, qualifiedTableName } from "@/lib/tableSelectSql"; import { useToast } from "@/composables/useToast"; import { buildExecutableObjectSourceSql, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor"; import { useQueryStore } from "@/stores/queryStore"; @@ -215,7 +215,15 @@ async function openSource(row: ObjectRow) { function openNewQuery(row: ObjectRow) { const tabId = queryStore.createTab(props.connection.id, props.database, row.name); - queryStore.updateSql(tabId, `SELECT * FROM ${qualifiedName(row)} LIMIT 100;`); + queryStore.updateSql( + tabId, + buildTableSelectSql({ + databaseType: props.connection.db_type, + schema: row.schema || selectedSchema.value, + tableName: row.name, + limit: 100, + }), + ); } function qualifiedName(row: ObjectRow): string { diff --git a/src/components/sidebar/TreeItem.vue b/src/components/sidebar/TreeItem.vue index 18451f4d7c..834f77ec47 100644 --- a/src/components/sidebar/TreeItem.vue +++ b/src/components/sidebar/TreeItem.vue @@ -415,6 +415,7 @@ async function openData() { databaseType: config.db_type, schema: node.schema, tableName: node.label, + columns: columns.map((column) => column.name), primaryKeys: pks, includeRowId: usesSyntheticRowIdKey(config.db_type, pks), }); @@ -896,7 +897,20 @@ async function exportData(format: "csv" | "json" | "sql") { isSchemaAware(config.db_type) && node.schema ? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}` : quoteIdent(node.label); - const result = await api.executeQuery(node.connectionId, node.database, `SELECT * FROM ${qualifiedName}`); + const queryColumns = + config.db_type === "neo4j" + ? (await api.getColumns(node.connectionId, node.database, node.schema || node.database, node.label)).map( + (column) => column.name, + ) + : undefined; + const dataSql = buildTableSelectSql({ + databaseType: config.db_type, + schema: node.schema, + tableName: node.label, + columns: queryColumns, + limit: 10000, + }); + const result = await api.executeQuery(node.connectionId, node.database, dataSql); let content: string; let ext: string; @@ -927,11 +941,20 @@ async function exportDataXlsx() { try { await connectionStore.ensureConnected(node.connectionId); - const qualifiedName = - isSchemaAware(config.db_type) && node.schema - ? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}` - : quoteIdent(node.label); - const result = await api.executeQuery(node.connectionId, node.database, `SELECT * FROM ${qualifiedName}`); + const queryColumns = + config.db_type === "neo4j" + ? (await api.getColumns(node.connectionId, node.database, node.schema || node.database, node.label)).map( + (column) => column.name, + ) + : undefined; + const dataSql = buildTableSelectSql({ + databaseType: config.db_type, + schema: node.schema, + tableName: node.label, + columns: queryColumns, + limit: 10000, + }); + const result = await api.executeQuery(node.connectionId, node.database, dataSql); const { buildXlsxWorkbook } = await import("@/lib/xlsxExport"); const workbook = buildXlsxWorkbook({ diff --git a/src/composables/useDataGridActions.ts b/src/composables/useDataGridActions.ts index 46a2f33bfe..bf0bd35ca6 100644 --- a/src/composables/useDataGridActions.ts +++ b/src/composables/useDataGridActions.ts @@ -37,6 +37,7 @@ export function useDataGridActions(activeTab: ComputedRef) databaseType: config?.db_type, schema: tab.tableMeta?.schema, tableName: tab.tableMeta?.tableName ?? "", + columns: tab.tableMeta?.columns.map((column) => column.name), primaryKeys, fallbackOrderColumns, includeRowId: useRowId, @@ -100,7 +101,11 @@ export function useDataGridActions(activeTab: ComputedRef) if (tab.mode === "data") { if (!tab.tableMeta) return; tab.whereInput = whereInput ?? ""; - const orderBy = direction ? `${quoteIdent(tab, column)} ${direction.toUpperCase()}` : undefined; + const config = connectionStore.getConfig(tab.connectionId); + const quotedColumn = quoteIdent(tab, column); + const orderBy = direction + ? `${config?.db_type === "neo4j" ? `n.${quotedColumn}` : quotedColumn} ${direction.toUpperCase()}` + : undefined; const sql = buildTableSql(tab, { orderBy, whereInput }); queryStore.updateSql(tab.id, sql); await queryStore.executeCurrentTab(); diff --git a/src/composables/useNavigationTargets.ts b/src/composables/useNavigationTargets.ts index 51270d722c..2f2bcac9fb 100644 --- a/src/composables/useNavigationTargets.ts +++ b/src/composables/useNavigationTargets.ts @@ -27,6 +27,27 @@ async function openTableTarget(target: NavigationTarget) { await connectionStore.ensureConnected(target.connectionId); if (!config) throw new Error("Connection config not found"); const querySchema = target.schema || target.database; + if (config.db_type === "neo4j") { + const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName); + const primaryKeys = editablePrimaryKeys(config.db_type, columns); + const sql = buildTableSelectSql({ + databaseType: config.db_type, + schema: target.schema, + tableName: target.tableName, + columns: columns.map((column) => column.name), + primaryKeys, + whereInput: target.whereInput, + }); + queryStore.updateSql(tabId, sql); + queryStore.setTableMeta(tabId, { + schema: target.schema, + tableName: target.tableName, + columns, + primaryKeys, + }); + await queryStore.executeTabSql(tabId, sql); + return; + } const sql = buildTableSelectSql({ databaseType: config.db_type, schema: target.schema, diff --git a/src/lib/dataGridSql.ts b/src/lib/dataGridSql.ts index 04291e57a6..150917702c 100644 --- a/src/lib/dataGridSql.ts +++ b/src/lib/dataGridSql.ts @@ -1,5 +1,5 @@ import type { DatabaseType } from "@/types/database"; -import { DBX_ROWID_COLUMN } from "./tableEditing.ts"; +import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN } from "./tableEditing.ts"; import { qualifiedTableName, quoteTableIdentifier } from "./tableSelectSql.ts"; export type GridCellValue = string | number | boolean | null; @@ -70,6 +70,8 @@ export function validateDataGridSave(options: DataGridSaveValidationOptions): st } export function buildDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] { + if (options.databaseType === "neo4j") return buildNeo4jDataGridSaveStatements(options); + const table = qualifiedTableName({ databaseType: options.databaseType, schema: options.tableMeta.schema, @@ -114,6 +116,8 @@ export function buildDataGridSaveStatements(options: DataGridSaveStatementOption } export function buildDataGridRollbackStatements(options: DataGridSaveStatementOptions): string[] { + if (options.databaseType === "neo4j") return buildNeo4jDataGridRollbackStatements(options); + const table = qualifiedTableName({ databaseType: options.databaseType, schema: options.tableMeta.schema, @@ -172,6 +176,7 @@ export function dataGridSaveExecutionSchema( databaseType: DatabaseType | undefined, tableMeta: DataGridTableMeta | undefined, ): string | undefined { + if (databaseType === "neo4j") return undefined; if (databaseType === "oracle") return undefined; return tableMeta?.schema; } @@ -219,6 +224,10 @@ function isOracleRowId(databaseType: DatabaseType | undefined, name: string | un return databaseType === "oracle" && name?.toUpperCase() === DBX_ROWID_COLUMN; } +function isNeo4jElementId(databaseType: DatabaseType | undefined, name: string | undefined): boolean { + return databaseType === "neo4j" && name === DBX_NEO4J_ELEMENT_ID_COLUMN; +} + function isAutoGeneratedColumn(column: DataGridColumnInfo): boolean { return /\b(auto_increment|autoincrement|identity)\b/i.test(column.extra ?? ""); } @@ -230,6 +239,7 @@ function isNullWriteToNotNullColumn( value: GridCellValue, ): boolean { if (!column || isOracleRowId(databaseType, column)) return false; + if (isNeo4jElementId(databaseType, column)) return false; return value === null && notNullColumns.has(normalizeColumnName(column)); } @@ -248,3 +258,98 @@ function predicateIdent(databaseType: DatabaseType | undefined, name: string): s function quoteIdent(databaseType: DatabaseType | undefined, name: string): string { return quoteTableIdentifier(databaseType, name); } + +function buildNeo4jDataGridSaveStatements(options: DataGridSaveStatementOptions): string[] { + const label = quoteIdent("neo4j", options.tableMeta.tableName); + const statements: string[] = []; + + for (const [rowIndex, changes] of options.dirtyRows) { + const row = options.rows[rowIndex]; + if (!row) continue; + const sets = changes + .filter(([columnIndex]) => !isNeo4jElementId("neo4j", options.columns[columnIndex])) + .map( + ([columnIndex, value]) => + `n.${quoteIdent("neo4j", options.columns[columnIndex])} = ${formatGridSqlLiteral(value, "neo4j")}`, + ) + .join(", "); + if (!sets) continue; + statements.push(`MATCH (n:${label}) WHERE ${neo4jElementIdPredicate(options.columns, row)} SET ${sets};`); + } + + for (const rowIndex of options.deletedRows) { + const row = options.rows[rowIndex]; + if (!row) continue; + statements.push(`MATCH (n:${label}) WHERE ${neo4jElementIdPredicate(options.columns, row)} DETACH DELETE n;`); + } + + for (const row of options.newRows) { + const props = options.columns + .map((column, index) => ({ column, value: row[index] })) + .filter((pair) => !isNeo4jElementId("neo4j", pair.column)) + .filter((pair) => pair.value !== null && pair.value !== undefined) + .map((pair) => `${quoteIdent("neo4j", pair.column)}: ${formatGridSqlLiteral(pair.value, "neo4j")}`) + .join(", "); + statements.push(props ? `CREATE (n:${label} {${props}});` : `CREATE (n:${label});`); + } + + return statements; +} + +function buildNeo4jDataGridRollbackStatements(options: DataGridSaveStatementOptions): string[] { + const label = quoteIdent("neo4j", options.tableMeta.tableName); + const statements: string[] = []; + + for (const row of options.newRows) { + const where = neo4jRowPropertyWhere(options.columns, row); + statements.push( + where ? `MATCH (n:${label}) WHERE ${where} DETACH DELETE n;` : `MATCH (n:${label}) DETACH DELETE n;`, + ); + } + + for (const rowIndex of options.deletedRows) { + const row = options.rows[rowIndex]; + if (!row) continue; + const props = options.columns + .map((column, index) => ({ column, value: row[index] })) + .filter((pair) => !isNeo4jElementId("neo4j", pair.column)) + .filter((pair) => pair.value !== null && pair.value !== undefined) + .map((pair) => `${quoteIdent("neo4j", pair.column)}: ${formatGridSqlLiteral(pair.value, "neo4j")}`) + .join(", "); + statements.push(props ? `CREATE (n:${label} {${props}});` : `CREATE (n:${label});`); + } + + for (const [rowIndex, changes] of options.dirtyRows) { + const row = options.rows[rowIndex]; + if (!row) continue; + const sets = changes + .filter(([columnIndex]) => !isNeo4jElementId("neo4j", options.columns[columnIndex])) + .map( + ([columnIndex]) => + `n.${quoteIdent("neo4j", options.columns[columnIndex])} = ${formatGridSqlLiteral(row[columnIndex], "neo4j")}`, + ) + .join(", "); + if (!sets) continue; + statements.push(`MATCH (n:${label}) WHERE ${neo4jElementIdPredicate(options.columns, row)} SET ${sets};`); + } + + return statements; +} + +function neo4jElementIdPredicate(columns: string[], row: GridCellValue[]): string { + const value = row[columns.indexOf(DBX_NEO4J_ELEMENT_ID_COLUMN)]; + return `elementId(n) = ${formatGridSqlLiteral(value, "neo4j")}`; +} + +function neo4jRowPropertyWhere(columns: string[], row: GridCellValue[]): string { + return columns + .map((column, index) => { + if (isNeo4jElementId("neo4j", column)) return ""; + const value = row[index]; + const ident = `n.${quoteIdent("neo4j", column)}`; + if (value === null || value === undefined) return `${ident} IS NULL`; + return `${ident} = ${formatGridSqlLiteral(value, "neo4j")}`; + }) + .filter(Boolean) + .join(" AND "); +} diff --git a/src/lib/tableEditing.ts b/src/lib/tableEditing.ts index 9b2498275e..e1a7d3cb0c 100644 --- a/src/lib/tableEditing.ts +++ b/src/lib/tableEditing.ts @@ -1,15 +1,21 @@ import type { ColumnInfo, DatabaseType } from "@/types/database"; export const DBX_ROWID_COLUMN = "__DBX_ROWID"; +export const DBX_NEO4J_ELEMENT_ID_COLUMN = "__DBX_ELEMENT_ID"; export function editablePrimaryKeys(databaseType: DatabaseType | undefined, columns: ColumnInfo[]): string[] { const primaryKeys = columns.filter((column) => column.is_primary_key).map((column) => column.name); if (databaseType === "oracle" && primaryKeys.length === 0) return [DBX_ROWID_COLUMN]; + if (databaseType === "neo4j" && primaryKeys.length === 0) return [DBX_NEO4J_ELEMENT_ID_COLUMN]; return primaryKeys; } export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[]): boolean { - return databaseType === "oracle" && primaryKeys.length === 1 && primaryKeys[0].toUpperCase() === DBX_ROWID_COLUMN; + return ( + primaryKeys.length === 1 && + ((databaseType === "oracle" && primaryKeys[0].toUpperCase() === DBX_ROWID_COLUMN) || + (databaseType === "neo4j" && primaryKeys[0] === DBX_NEO4J_ELEMENT_ID_COLUMN)) + ); } export function isHiddenGridColumn( @@ -17,5 +23,6 @@ export function isHiddenGridColumn( column: string, primaryKeys: string[], ): boolean { + if (databaseType === "neo4j" && column === DBX_NEO4J_ELEMENT_ID_COLUMN) return true; return usesSyntheticRowIdKey(databaseType, primaryKeys) && column.toUpperCase() === DBX_ROWID_COLUMN; } diff --git a/src/lib/tableSelectSql.ts b/src/lib/tableSelectSql.ts index 05c498fae8..23be73701d 100644 --- a/src/lib/tableSelectSql.ts +++ b/src/lib/tableSelectSql.ts @@ -1,12 +1,13 @@ import type { DatabaseType } from "../types/database.ts"; import { isSchemaAware, usesFetchFirst } from "./databaseCapabilities.ts"; -import { DBX_ROWID_COLUMN } from "./tableEditing.ts"; +import { DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN } from "./tableEditing.ts"; export interface BuildTableSelectSqlOptions { databaseType?: DatabaseType; schema?: string; tableName: string; primaryKeys?: string[]; + columns?: string[]; fallbackOrderColumns?: string[]; orderBy?: string; limit?: number; @@ -17,10 +18,15 @@ export interface BuildTableSelectSqlOptions { export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string { if (databaseType === "mysql") return `\`${name.replace(/`/g, "``")}\``; + if (databaseType === "neo4j") return quoteCypherIdentifier(name); if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`; return `"${name.replace(/"/g, '""')}"`; } +function quoteCypherIdentifier(name: string): string { + return `\`${name.replace(/`/g, "``")}\``; +} + function isOracleRowId(databaseType: DatabaseType | undefined, name: string): boolean { return databaseType === "oracle" && name.toUpperCase() === DBX_ROWID_COLUMN; } @@ -48,6 +54,8 @@ export function normalizeWhereInput(whereInput?: string): string { export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string { const databaseType = options.databaseType; const limit = options.limit ?? 100; + if (databaseType === "neo4j") return buildNeo4jTableSelectSql(options, limit); + const table = qualifiedTableName(options); const predicate = normalizeWhereInput(options.whereInput); const where = predicate ? ` WHERE (${predicate})` : ""; @@ -77,3 +85,22 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string const offset = options.offset ? ` OFFSET ${options.offset}` : ""; return `SELECT * FROM ${table}${where}${order} LIMIT ${limit}${offset};`; } + +function buildNeo4jTableSelectSql(options: BuildTableSelectSqlOptions, limit: number): string { + const label = quoteCypherIdentifier(options.tableName); + const predicate = normalizeWhereInput(options.whereInput); + const where = predicate ? ` WHERE ${predicate}` : ""; + const returnedColumns = options.columns?.length + ? options.columns + .map((column) => `n.${quoteCypherIdentifier(column)} AS ${quoteCypherIdentifier(column)}`) + .join(", ") + : "n"; + const returns = `elementId(n) AS ${quoteCypherIdentifier(DBX_NEO4J_ELEMENT_ID_COLUMN)}, ${returnedColumns}`; + const defaultOrderBy = options.primaryKeys?.length + ? options.primaryKeys.map((pk) => `n.${quoteCypherIdentifier(pk)} ASC`).join(", ") + : undefined; + const orderBy = options.orderBy ?? defaultOrderBy; + const order = orderBy ? ` ORDER BY ${orderBy}` : ""; + const skip = options.offset ? ` SKIP ${options.offset}` : ""; + return `MATCH (n:${label})${where} RETURN ${returns}${order}${skip} LIMIT ${limit};`; +} diff --git a/tests/dataGridSql.test.ts b/tests/dataGridSql.test.ts index f802fd5c03..d7c4ba4c76 100644 --- a/tests/dataGridSql.test.ts +++ b/tests/dataGridSql.test.ts @@ -1,10 +1,12 @@ import { strict as assert } from "node:assert"; import test from "node:test"; import { + buildDataGridRollbackStatements, buildDataGridSaveStatements, dataGridSaveExecutionSchema, validateDataGridSave, } from "../src/lib/dataGridSql.ts"; +import { DBX_NEO4J_ELEMENT_ID_COLUMN } from "../src/lib/tableEditing.ts"; test("builds SQL Server grid save statements with schema and bracket quoting", () => { const statements = buildDataGridSaveStatements({ @@ -58,6 +60,55 @@ test("uses Oracle ROWID as a synthetic key without writing it as a normal column ]); }); +test("builds Neo4j grid save statements with elementId keys", () => { + const statements = buildDataGridSaveStatements({ + databaseType: "neo4j", + tableMeta: { + tableName: "Employee", + primaryKeys: [DBX_NEO4J_ELEMENT_ID_COLUMN], + }, + columns: [DBX_NEO4J_ELEMENT_ID_COLUMN, "name", "active"], + rows: [["4:abc:7", "Ada", true]], + dirtyRows: [[0, [[1, "Grace"]]]], + deletedRows: [0], + newRows: [[null, "Linus", false]], + }); + + assert.deepEqual(statements, [ + "MATCH (n:`Employee`) WHERE elementId(n) = '4:abc:7' SET n.`name` = 'Grace';", + "MATCH (n:`Employee`) WHERE elementId(n) = '4:abc:7' DETACH DELETE n;", + "CREATE (n:`Employee` {`name`: 'Linus', `active`: FALSE});", + ]); +}); + +test("builds best-effort Neo4j rollback statements", () => { + const statements = buildDataGridRollbackStatements({ + databaseType: "neo4j", + tableMeta: { + tableName: "Employee", + primaryKeys: [DBX_NEO4J_ELEMENT_ID_COLUMN], + }, + columns: [DBX_NEO4J_ELEMENT_ID_COLUMN, "name", "active"], + rows: [["4:abc:7", "Ada", true]], + dirtyRows: [[0, [[1, "Grace"]]]], + deletedRows: [0], + newRows: [[null, "Linus", false]], + }); + + assert.deepEqual(statements, [ + "MATCH (n:`Employee`) WHERE n.`name` = 'Linus' AND n.`active` = FALSE DETACH DELETE n;", + "CREATE (n:`Employee` {`name`: 'Ada', `active`: TRUE});", + "MATCH (n:`Employee`) WHERE elementId(n) = '4:abc:7' SET n.`name` = 'Ada';", + ]); +}); + +test("skips schema setup for Neo4j data grid saves", () => { + assert.equal( + dataGridSaveExecutionSchema("neo4j", { schema: "ignored", tableName: "Employee", primaryKeys: [] }), + undefined, + ); +}); + test("skips current_schema setup for Oracle data grid saves", () => { assert.equal( dataGridSaveExecutionSchema("oracle", { schema: "DBXTEST", tableName: "T", primaryKeys: [] }), diff --git a/tests/tableEditing.test.ts b/tests/tableEditing.test.ts index fb566f8fb6..416c85bf1a 100644 --- a/tests/tableEditing.test.ts +++ b/tests/tableEditing.test.ts @@ -1,6 +1,7 @@ import { strict as assert } from "node:assert"; import test from "node:test"; import { + DBX_NEO4J_ELEMENT_ID_COLUMN, DBX_ROWID_COLUMN, editablePrimaryKeys, isHiddenGridColumn, @@ -31,15 +32,21 @@ test("does not synthesize ROWID for non-Oracle keyless tables", () => { assert.deepEqual(editablePrimaryKeys("mysql", [column("ID"), column("CITY")]), []); }); +test("uses elementId as Neo4j editable key when labels have no primary key", () => { + assert.deepEqual(editablePrimaryKeys("neo4j", [column("name"), column("role")]), [DBX_NEO4J_ELEMENT_ID_COLUMN]); +}); + test("detects the synthetic Oracle ROWID key case", () => { assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN]), true); assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN.toLowerCase()]), true); assert.equal(usesSyntheticRowIdKey("postgres", [DBX_ROWID_COLUMN]), false); assert.equal(usesSyntheticRowIdKey("oracle", ["ID"]), false); + assert.equal(usesSyntheticRowIdKey("neo4j", [DBX_NEO4J_ELEMENT_ID_COLUMN]), true); }); test("hides only the synthetic Oracle ROWID grid column", () => { assert.equal(isHiddenGridColumn("oracle", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN]), true); assert.equal(isHiddenGridColumn("oracle", "ROWID", [DBX_ROWID_COLUMN]), false); assert.equal(isHiddenGridColumn("mysql", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN]), false); + assert.equal(isHiddenGridColumn("neo4j", DBX_NEO4J_ELEMENT_ID_COLUMN, [DBX_NEO4J_ELEMENT_ID_COLUMN]), true); }); diff --git a/tests/tableSelectSql.test.ts b/tests/tableSelectSql.test.ts index c52c0760e1..c8f1742c17 100644 --- a/tests/tableSelectSql.test.ts +++ b/tests/tableSelectSql.test.ts @@ -85,3 +85,30 @@ test("builds Oracle table data queries with ROWID for keyless editing", () => { `SELECT ROWIDTOCHAR(t.ROWID) AS "__DBX_ROWID", t.* FROM "DBXTEST"."DBX_LOAD_TABLE_006" t ORDER BY t.ROWID ASC FETCH FIRST 100 ROWS ONLY`, ); }); + +test("builds Neo4j table data queries as Cypher label matches", () => { + const sql = buildTableSelectSql({ + databaseType: "neo4j", + tableName: "Employee", + primaryKeys: ["id"], + limit: 100, + offset: 200, + }); + + assert.equal(sql, "MATCH (n:`Employee`) RETURN elementId(n) AS `__DBX_ELEMENT_ID`, n ORDER BY n.`id` ASC SKIP 200 LIMIT 100;"); +}); + +test("expands Neo4j table data queries into node property columns", () => { + const sql = buildTableSelectSql({ + databaseType: "neo4j", + tableName: "Employee", + columns: ["id", "first name", "role"], + primaryKeys: ["id"], + limit: 100, + }); + + assert.equal( + sql, + "MATCH (n:`Employee`) RETURN elementId(n) AS `__DBX_ELEMENT_ID`, n.`id` AS `id`, n.`first name` AS `first name`, n.`role` AS `role` ORDER BY n.`id` ASC LIMIT 100;", + ); +});