fix(neo4j): pick node identity function by server version

This commit is contained in:
zipg
2026-09-28 23:01:49 +08:00
committed by GitHub
parent 9bb52c198f
commit 625d84bb4a
25 changed files with 461 additions and 30 deletions
@@ -3895,6 +3895,7 @@ async function refreshSavedRows(request: { dirtyRows: ReadonlyMap<number, Readon
const sql = await buildTableSelectSql({
databaseType: resolvedDatabaseType.value,
driverProfile: connectionStore.getConfig(connectionId)?.driver_profile,
serverVersion: connectionStore.getConfig(connectionId)?.database_info?.productVersion,
identifierQuote: connectionStore.connectionIdentifierQuote?.(connectionId),
catalog: tableMeta.catalog,
database: tableMeta.database,
@@ -6547,6 +6548,7 @@ async function applyOrderBySearch() {
const sql = await buildTableSelectSql({
databaseType: resolvedDatabaseType.value,
driverProfile: props.connectionId ? connectionStore.getConfig(props.connectionId)?.driver_profile : undefined,
serverVersion: props.connectionId ? connectionStore.getConfig(props.connectionId)?.database_info?.productVersion : undefined,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId),
catalog: tableMeta.catalog,
database: tableMeta.database,
@@ -6586,6 +6588,7 @@ async function applyWhereFilter() {
const sql = await buildTableSelectSql({
databaseType: resolvedDatabaseType.value,
driverProfile: props.connectionId ? connectionStore.getConfig(props.connectionId)?.driver_profile : undefined,
serverVersion: props.connectionId ? connectionStore.getConfig(props.connectionId)?.database_info?.productVersion : undefined,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId),
catalog: tableMeta.catalog,
database: tableMeta.database,
@@ -7851,6 +7854,7 @@ async function syncUserFacingSql() {
const sql = await buildTableSelectSql({
databaseType: resolvedDatabaseType.value,
driverProfile: config?.driver_profile,
serverVersion: config?.database_info?.productVersion,
identifierQuote: props.connectionId ? connectionStore.connectionIdentifierQuote?.(props.connectionId) : undefined,
catalog: props.tableMeta.catalog,
database: props.tableMeta.database,
@@ -11341,6 +11345,7 @@ async function loadForeignKeyDisplayLabels() {
const sql = await buildTableSelectSql({
databaseType: resolvedDatabaseType.value,
driverProfile: connectionStore.getConfig(props.connectionId!)?.driver_profile,
serverVersion: connectionStore.getConfig(props.connectionId!)?.database_info?.productVersion,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId),
catalog: props.tableMeta?.catalog,
database: props.database,
@@ -1692,6 +1692,7 @@ async function openNewQuery(row: ObjectBrowserRow) {
await buildTableSelectSql({
databaseType: effectiveDatabaseType.value,
driverProfile: props.connection.driver_profile,
serverVersion: props.connection.database_info?.productVersion,
identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connection.id),
catalog: props.catalog,
database: props.database,
@@ -97,4 +97,10 @@ describe("useDataGridEditor save SQL database qualification", () => {
await createEditor(false).saveChanges();
expect(mocks.prepareDataGridSave.mock.calls[1]![0]).toMatchObject({ includeDatabaseName: false });
});
it("forwards the connected server version so saved statements match the read SQL (#10503)", async () => {
mocks.getConfig.mockReturnValue({ database_info: { productVersion: "Neo4j/4.4.44" } });
await createEditor(false).saveChanges();
expect(mocks.prepareDataGridSave.mock.calls[0]![0]).toMatchObject({ serverVersion: "Neo4j/4.4.44" });
});
});
@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
setErrorResult: vi.fn(),
setTableMeta: vi.fn(),
updateSql: vi.fn(),
databaseInfo: undefined as { productName?: string; productVersion?: string } | undefined,
}));
vi.mock("@/lib/backend/api", () => ({
@@ -27,7 +28,7 @@ vi.mock("@/lib/backend/api", () => ({
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
activeConnectionId: "",
getConfig: () => ({ id: "connection-1", db_type: mocks.databaseType }),
getConfig: () => ({ id: "connection-1", db_type: mocks.databaseType, database_info: mocks.databaseInfo }),
ensureConnected: mocks.ensureConnected,
connectionIdentifierQuote: () => undefined,
metadataGenerationFor: () => 0,
@@ -108,6 +109,7 @@ describe("useNavigationTargets openTableTarget", () => {
mocks.tabs.length = 0;
mocks.databaseType = "postgres";
mocks.tableOpenSortMode = "none";
mocks.databaseInfo = undefined;
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.buildTableSelectSql.mockImplementation(async ({ tableName }: { tableName: string }) => `SELECT * FROM ${tableName}`);
mocks.executeTabSql.mockResolvedValue(undefined);
@@ -130,6 +132,30 @@ describe("useNavigationTargets openTableTarget", () => {
expect(mocks.tabs[0]?.orderByInput).toBeUndefined();
});
it("forwards the connected server version to the table SQL builder (#10503)", async () => {
mocks.databaseType = "neo4j";
mocks.databaseInfo = { productName: "Neo4j", productVersion: "Neo4j/4.4.44" };
await useNavigationTargets(dialogs).openTableTarget({ connectionId: "connection-1", database: "neo4j", tableName: "users", tableType: "TABLE" });
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ serverVersion: "Neo4j/4.4.44" }));
});
it("reads the server version when the SQL is built, not from the config captured before connect (#10503)", async () => {
mocks.databaseType = "neo4j";
mocks.databaseInfo = undefined;
// The store swaps the config object when the probe lands, so the reference captured before
// connect never sees the version.
mocks.getColumns.mockImplementation(async () => {
mocks.databaseInfo = { productName: "Neo4j", productVersion: "Neo4j/4.4.44" };
return [column("id")];
});
await useNavigationTargets(dialogs).openTableTarget({ connectionId: "connection-1", database: "neo4j", tableName: "users", tableType: "TABLE" });
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ serverVersion: "Neo4j/4.4.44" }));
});
it.each(["oracle", "oceanbase-oracle"])("uses the %s physical primary index for default ordering", async (databaseType) => {
mocks.databaseType = databaseType;
mocks.tableOpenSortMode = "database";
@@ -122,6 +122,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
return buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config?.driver_profile,
serverVersion: config?.database_info?.productVersion,
identifierQuote: connectionStore.connectionIdentifierQuote?.(tab.connectionId),
database: tableMeta?.database,
schema: tableMeta?.schema,
@@ -1592,6 +1592,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
if (!tableMeta.value) return null;
return {
databaseType: resolvedDatabaseType.value,
serverVersion: connectionStore.getConfig(connectionId.value ?? "")?.database_info?.productVersion,
identifierQuote: connectionStore.connectionIdentifierQuote?.(connectionId.value),
tableMeta: tableMeta.value,
columns: result.value.columns,
@@ -258,6 +258,7 @@ export function useDataGridLargeValues(options: UseDataGridLargeValuesOptions) {
if (!visibleLargeValuePreviewActive || generation !== visibleLargeValuePreviewRequestedGeneration || options.result.value !== sourceResult) return;
const sql = await buildTableSelectSql({
databaseType: options.databaseType.value,
serverVersion: options.getConnectionConfig(options.connectionId.value!)?.database_info?.productVersion,
identifierQuote: options.connectionIdentifierQuote(options.connectionId.value),
database: tableMeta.database,
schema: tableMeta.schema,
@@ -426,6 +427,7 @@ export function useDataGridLargeValues(options: UseDataGridLargeValuesOptions) {
const selectedColumns = [...new Set([...tableMeta.primaryKeys, sourceColumn])];
const sql = await buildTableSelectSql({
databaseType: options.databaseType.value,
serverVersion: options.getConnectionConfig(options.connectionId.value!)?.database_info?.productVersion,
identifierQuote: options.connectionIdentifierQuote(options.connectionId.value),
database: tableMeta.database,
schema: tableMeta.schema,
@@ -147,6 +147,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config.driver_profile,
serverVersion: connectionStore.getConfig(target.connectionId)?.database_info?.productVersion,
identifierQuote,
schema: tableSchema,
catalog: target.catalog,
@@ -204,6 +205,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config.driver_profile,
serverVersion: connectionStore.getConfig(target.connectionId)?.database_info?.productVersion,
identifierQuote,
schema: tableSchema,
catalog: target.catalog,
@@ -255,6 +257,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
const emptySql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config.driver_profile,
serverVersion: connectionStore.getConfig(target.connectionId)?.database_info?.productVersion,
identifierQuote,
schema: tableSchema,
catalog: target.catalog,
@@ -302,6 +305,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
const newSql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config.driver_profile,
serverVersion: connectionStore.getConfig(target.connectionId)?.database_info?.productVersion,
identifierQuote,
schema: tableSchema,
catalog: target.catalog,
@@ -375,6 +375,7 @@ export function useSidebarDataOpenRuntime() {
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config?.driver_profile,
serverVersion: connectionStore.getConfig(node.connectionId)?.database_info?.productVersion,
identifierQuote: connectionStore.connectionIdentifierQuote?.(node.connectionId),
schema: tableSchema,
database: node.database,
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { joinedRowChanges, type JoinedWriteSource } from "../../dataGrid/joinedRowChanges";
import { joinedRowChanges, joinedSaveOptions, type JoinedWriteSource } from "../../dataGrid/joinedRowChanges";
const sources: JoinedWriteSource[] = [
{ tableKey: "users", primaryKeys: ["id"], sourceColumns: ["id", "name", undefined, undefined] },
@@ -79,3 +79,22 @@ describe("joined row updates", () => {
).toHaveLength(2);
});
});
describe("joined save options", () => {
it("keeps the connected server version so saved rows are addressed like the rows that were read", () => {
const options = joinedSaveOptions([{ tableMeta: { tableName: "events", primaryKeys: ["id"] }, sourceColumns: ["id", "name"] }], {
databaseType: "neo4j",
serverVersion: "Neo4j/4.4.44",
identifierQuote: "`",
includeDatabaseName: false,
columns: ["id", "name"],
rows: [[1, "user"]],
dirtyRows: new Map([[0, new Map([[1, "renamed"]])]]),
newRows: [],
deletedRows: new Set(),
});
expect(options).toHaveLength(1);
expect(options[0]!.serverVersion).toBe("Neo4j/4.4.44");
expect(options[0]!.databaseType).toBe("neo4j");
});
});
@@ -25,6 +25,9 @@ export interface DataGridColumnInfo {
export interface DataGridSaveStatementOptions {
databaseType?: DatabaseType;
/** Server version reported by the connection (see `BuildTableSelectSqlOptions.serverVersion`).
* The saved statements must address rows the same way the grid read them. */
serverVersion?: string;
identifierQuote?: string;
tableMeta: DataGridTableMeta;
columns: string[];
@@ -9,7 +9,7 @@ export interface JoinedSaveTarget {
/** Feed per-source changes through the existing SQL builder, including undo SQL. */
export function joinedSaveOptions(
targets: JoinedSaveTarget[],
options: Pick<DataGridSaveStatementOptions, "databaseType" | "identifierQuote" | "columns" | "includeDatabaseName"> & {
options: Pick<DataGridSaveStatementOptions, "databaseType" | "serverVersion" | "identifierQuote" | "columns" | "includeDatabaseName"> & {
rows: CellValue[][];
dirtyRows: Map<number, Map<number, CellValue>>;
newRows: CellValue[][];
@@ -24,6 +24,7 @@ export function joinedSaveOptions(
}));
return joinedRowChanges(sources, options.rows, options.dirtyRows).map((update) => ({
databaseType: options.databaseType,
serverVersion: options.serverVersion,
identifierQuote: options.identifierQuote,
columns: options.columns,
tableMeta: targets[update.sourceIndex]!.tableMeta,
@@ -13,6 +13,10 @@ import type { SqlSemanticToken } from "@/lib/sql/semantic/types";
export interface BuildTableSelectSqlOptions {
databaseType?: DatabaseType;
driverProfile?: string;
/** Server version reported by the connection (for example `Neo4j/4.4.44`). Engines that
* renamed a built-in function across releases (Neo4j below 5 uses `id()` where 5+ uses
* `elementId()`) need it to generate SQL the connected server understands. */
serverVersion?: string;
identifierQuote?: string;
schema?: string;
tableName: string;
+2
View File
@@ -4842,6 +4842,7 @@ export const useQueryStore = defineStore("query", () => {
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: conn?.driver_profile,
serverVersion: conn?.database_info?.productVersion,
identifierQuote,
database: tableMeta.database,
schema: tableMeta.schema,
@@ -8962,6 +8963,7 @@ export const useQueryStore = defineStore("query", () => {
const sql = await api.buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: conn?.driver_profile,
serverVersion: conn?.database_info?.productVersion,
identifierQuote,
database: tableMeta.database,
schema: tableMeta.schema,
+38 -6
View File
@@ -1491,7 +1491,8 @@ async fn execute_get_sample_data(
// Reuse the table-data builder so identifier quoting and row limiting follow
// the active database instead of assuming PostgreSQL syntax.
let sql = build_sample_data_sql(db_type, schema.as_deref(), table, limit);
let server_version = connection_server_version(state, connection_id).await;
let sql = build_sample_data_sql(db_type, server_version.as_deref(), schema.as_deref(), table, limit);
// Delegate to execute_execute_query with a synthetic tool call
let synthetic_call = ToolCall {
@@ -1512,9 +1513,28 @@ async fn execute_get_sample_data(
.await
}
fn build_sample_data_sql(db_type: &DatabaseType, schema: Option<&str>, table: &str, limit: usize) -> String {
/// The sample-data statement is produced by the shared dialect code, so it has to match the
/// connected server too: Neo4j below 5 only accepts `id()` where 5+ uses `elementId()`.
async fn connection_server_version(state: &Arc<AppState>, connection_id: &str) -> Option<String> {
state
.configs
.read()
.await
.get(connection_id)
.and_then(|config| config.database_info.as_ref())
.and_then(|info| info.product_version.clone())
}
fn build_sample_data_sql(
db_type: &DatabaseType,
server_version: Option<&str>,
schema: Option<&str>,
table: &str,
limit: usize,
) -> String {
build_table_data_select_sql(TableDataSelectSqlOptions {
database_type: Some(*db_type),
server_version: server_version.map(str::to_owned),
schema: schema.map(str::to_owned),
table_name: table.to_string(),
limit: Some(limit),
@@ -2640,23 +2660,35 @@ for line in sys.stdin:
#[test]
fn sample_data_sql_uses_database_identifier_and_limit_syntax() {
assert_eq!(
build_sample_data_sql(&DatabaseType::Mysql, Some("app"), "sys_tenant", 20),
build_sample_data_sql(&DatabaseType::Mysql, None, Some("app"), "sys_tenant", 20),
"SELECT * FROM `app`.`sys_tenant` LIMIT 20;"
);
assert_eq!(
build_sample_data_sql(&DatabaseType::Postgres, Some("public"), "sys_tenant", 20),
build_sample_data_sql(&DatabaseType::Postgres, None, Some("public"), "sys_tenant", 20),
"SELECT * FROM \"public\".\"sys_tenant\" LIMIT 20;"
);
assert_eq!(
build_sample_data_sql(&DatabaseType::SqlServer, Some("dbo"), "sys_tenant", 20),
build_sample_data_sql(&DatabaseType::SqlServer, None, Some("dbo"), "sys_tenant", 20),
"SELECT TOP (20) * FROM [dbo].[sys_tenant]"
);
assert_eq!(
build_sample_data_sql(&DatabaseType::Oracle, Some("APP"), "SYS_TENANT", 20),
build_sample_data_sql(&DatabaseType::Oracle, None, Some("APP"), "SYS_TENANT", 20),
"SELECT * FROM (SELECT * FROM \"APP\".\"SYS_TENANT\") WHERE ROWNUM <= 20"
);
}
#[test]
fn sample_data_sql_follows_the_connected_neo4j_identifier_function() {
assert_eq!(
build_sample_data_sql(&DatabaseType::Neo4j, Some("Neo4j/4.4.44"), None, "Person", 20),
"MATCH (n:`Person`) RETURN id(n) AS `__DBX_ELEMENT_ID`, n LIMIT 20;"
);
assert_eq!(
build_sample_data_sql(&DatabaseType::Neo4j, Some("Neo4j/5.26.0"), None, "Person", 20),
"MATCH (n:`Person`) RETURN elementId(n) AS `__DBX_ELEMENT_ID`, n LIMIT 20;"
);
}
#[test]
fn selected_schema_overrides_the_tool_schema_argument() {
let call = ToolCall {
+54 -8
View File
@@ -36,18 +36,27 @@ pub struct TableCsvExportOptions {
pub null_literal: String,
}
async fn connection_database_type(state: &AppState, connection_id: &str) -> Result<DatabaseType, String> {
/// Export pages go through the same dialect code as the grid, so the statement has to match the
/// connected server too: Neo4j below 5 only accepts `id()` where 5+ uses `elementId()`. That needs
/// the recorded version next to the connection type.
async fn connection_server_version_and_type(
state: &AppState,
connection_id: &str,
) -> Result<(Option<String>, DatabaseType), String> {
state
.configs
.read()
.await
.get(connection_id)
.map(|config| config.db_type)
.map(|config| {
let server_version = config.database_info.as_ref().and_then(|info| info.product_version.clone());
(server_version, config.db_type)
})
.ok_or_else(|| format!("Connection config not found: {connection_id}"))
}
pub async fn export_table_data_csv_core(state: &AppState, options: TableCsvExportOptions) -> Result<u64, String> {
let database_type = connection_database_type(state, &options.connection_id).await?;
let (server_version, database_type) = connection_server_version_and_type(state, &options.connection_id).await?;
// This loop pages with `LIMIT <page_size> OFFSET <n>` and stops at the first
// short page. Neither half holds for SOQL: a `/query` response carries at most
// 2000 rows and hands the rest back as a QueryLocator (`has_more` +
@@ -67,6 +76,7 @@ pub async fn export_table_data_csv_core(state: &AppState, options: TableCsvExpor
let outcome = write_table_csv_pages(
&mut writer,
database_type,
server_version.as_deref(),
&options,
client_session_id.as_deref(),
|sql, query_options| async move {
@@ -93,6 +103,7 @@ pub async fn export_table_data_csv_core(state: &AppState, options: TableCsvExpor
async fn write_table_csv_pages<Execute, QueryFuture>(
writer: &mut impl Write,
database_type: DatabaseType,
server_version: Option<&str>,
options: &TableCsvExportOptions,
client_session_id: Option<&str>,
mut execute_page: Execute,
@@ -113,6 +124,7 @@ where
loop {
let sql = build_table_data_select_sql(TableDataSelectSqlOptions {
database_type: Some(database_type),
server_version: server_version.map(str::to_owned),
schema: options.schema.clone(),
table_name: options.table_name.clone(),
table_type: None,
@@ -225,6 +237,7 @@ mod tests {
let count = write_table_csv_pages(
&mut output,
DatabaseType::Cassandra,
None,
&export_options(usize::MAX),
Some("export-test"),
|sql, options| {
@@ -266,6 +279,7 @@ mod tests {
let count = write_table_csv_pages(
&mut Vec::new(),
DatabaseType::Cassandra,
None,
&export_options(2),
Some("export-test"),
|_, _| {
@@ -289,6 +303,7 @@ mod tests {
let error = write_table_csv_pages(
&mut Vec::new(),
DatabaseType::Cassandra,
None,
&export_options(2),
Some("export-test"),
|_, _| {
@@ -309,6 +324,7 @@ mod tests {
let error = write_table_csv_pages(
&mut Vec::new(),
DatabaseType::Cassandra,
None,
&export_options(2),
Some("export-test"),
|_, options| {
@@ -331,23 +347,53 @@ mod tests {
async fn non_cassandra_csv_export_preserves_limit_offset_paging() {
let mut pages = vec![page(&[1, 2], None, false), page(&[3], None, false)].into_iter();
let mut sqls = Vec::new();
let count =
write_table_csv_pages(&mut Vec::new(), DatabaseType::Sqlite, &export_options(2), None, |sql, options| {
let count = write_table_csv_pages(
&mut Vec::new(),
DatabaseType::Sqlite,
None,
&export_options(2),
None,
|sql, options| {
sqls.push(sql);
assert_eq!(options.max_rows, Some(2));
assert!(options.page_size.is_none());
assert!(options.result_session_id.is_none());
assert!(options.client_session_id.is_none());
std::future::ready(Ok(pages.next().unwrap()))
})
.await
.unwrap();
},
)
.await
.unwrap();
assert_eq!(count, 3);
assert_eq!(sqls.len(), 2);
assert!(sqls[0].contains("LIMIT 2"));
assert!(sqls[1].contains("OFFSET 2"));
}
#[tokio::test]
async fn csv_export_uses_the_identifier_function_the_neo4j_server_understands() {
for (server_version, identifier) in [("Neo4j/4.4.44", "id(n)"), ("Neo4j/5.26.0", "elementId(n)")] {
let mut pages = vec![page(&[1], None, false)].into_iter();
let mut sqls = Vec::new();
let options = export_options(2);
write_table_csv_pages(
&mut Vec::new(),
DatabaseType::Neo4j,
Some(server_version),
&options,
None,
|sql, _| {
sqls.push(sql);
std::future::ready(Ok(pages.next().expect("unexpected extra export page")))
},
)
.await
.unwrap();
assert_eq!(sqls.len(), 1);
assert!(sqls[0].contains(identifier), "{server_version} export SQL was {}", sqls[0]);
}
}
fn salesforce_config(id: &str) -> ConnectionConfig {
let mut config = serde_json::from_value::<ConnectionConfig>(serde_json::json!({
"id": id,
@@ -55,6 +55,7 @@ fn mysql_cross_database_query_flow_preserves_target_database() {
let save = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -244,6 +244,7 @@ mod tests {
DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Iotdb),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
+37 -10
View File
@@ -2,9 +2,11 @@ use serde_json::Value;
use super::*;
use crate::models::connection::DatabaseType;
use crate::sql_dialect::{neo4j_element_id_function, NEO4J_LEGACY_ELEMENT_ID_FUNCTION};
pub(super) fn build_neo4j_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Vec<String> {
let label = quote_ident(Some(DatabaseType::Neo4j), &options.table_meta.table_name);
let element_id_function = neo4j_element_id_function(options.server_version.as_deref());
let mut statements = Vec::new();
for (row_index, changes) in &options.dirty_rows {
@@ -29,8 +31,10 @@ pub(super) fn build_neo4j_data_grid_save_statements(options: &DataGridSaveStatem
if sets.is_empty() {
continue;
}
statements
.push(format!("MATCH (n:{label}) WHERE {} SET {sets};", neo4j_element_id_predicate(&options.columns, row)));
statements.push(format!(
"MATCH (n:{label}) WHERE {} SET {sets};",
neo4j_element_id_predicate(&options.columns, row, element_id_function)
));
}
for row_index in &options.deleted_rows {
@@ -39,7 +43,7 @@ pub(super) fn build_neo4j_data_grid_save_statements(options: &DataGridSaveStatem
};
statements.push(format!(
"MATCH (n:{label}) WHERE {} DETACH DELETE n;",
neo4j_element_id_predicate(&options.columns, row)
neo4j_element_id_predicate(&options.columns, row, element_id_function)
));
}
@@ -74,6 +78,7 @@ pub(super) fn build_neo4j_data_grid_save_statements(options: &DataGridSaveStatem
pub(super) fn build_neo4j_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -> Vec<String> {
let label = quote_ident(Some(DatabaseType::Neo4j), &options.table_meta.table_name);
let element_id_function = neo4j_element_id_function(options.server_version.as_deref());
let mut statements = Vec::new();
for row in &options.new_rows {
@@ -140,19 +145,41 @@ pub(super) fn build_neo4j_data_grid_rollback_statements(options: &DataGridSaveSt
if sets.is_empty() {
continue;
}
statements
.push(format!("MATCH (n:{label}) WHERE {} SET {sets};", neo4j_element_id_predicate(&options.columns, row)));
statements.push(format!(
"MATCH (n:{label}) WHERE {} SET {sets};",
neo4j_element_id_predicate(&options.columns, row, element_id_function)
));
}
statements
}
fn neo4j_element_id_predicate(columns: &[String], row: &[Value]) -> String {
fn neo4j_element_id_predicate(columns: &[String], row: &[Value], element_id_function: &str) -> String {
let index = columns.iter().position(|column| column == DBX_NEO4J_ELEMENT_ID_COLUMN).unwrap_or(usize::MAX);
format!(
"elementId(n) = {}",
format_grid_sql_literal(row.get(index).unwrap_or(&Value::Null), Some(DatabaseType::Neo4j), None)
)
let value = row.get(index).unwrap_or(&Value::Null);
// The legacy `id()` returns an Integer while a grid cell always carries text. Comparing the two
// with a quoted literal matches nothing, so a user edit would quietly update zero rows; the
// identity has to be written as a number there. `elementId()` keeps its string form.
if element_id_function == NEO4J_LEGACY_ELEMENT_ID_FUNCTION {
if let Some(identity) = neo4j_legacy_element_id_literal(value) {
return format!("{element_id_function}(n) = {identity}");
}
}
format!("{element_id_function}(n) = {}", format_grid_sql_literal(value, Some(DatabaseType::Neo4j), None))
}
/// Renders a legacy `id()` value as a Cypher number so it matches the server's Integer. Anything
/// that is not an integral id is left to the caller's normal literal formatting.
fn neo4j_legacy_element_id_literal(value: &Value) -> Option<String> {
match value {
Value::Number(number) => number.as_i64().map(|identity| identity.to_string()),
Value::String(text) => {
let trimmed = text.trim();
(!trimmed.is_empty() && trimmed.chars().all(|character| character.is_ascii_digit()))
.then(|| trimmed.to_string())
}
_ => None,
}
}
fn neo4j_row_property_where(columns: &[String], row: &[Value]) -> String {
@@ -196,6 +196,7 @@ mod tests {
DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Salesforce),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
database: None,
catalog: None,
@@ -234,6 +235,7 @@ mod tests {
DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Salesforce),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
database: None,
catalog: None,
+175
View File
@@ -98,6 +98,11 @@ pub struct DataGridSaveStatementOptions {
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier_quote: Option<String>,
/// Server version reported by the connection (for example `Neo4j/4.4.44`). Neo4j renamed the
/// node identity function in 5.0, so the saved statements must address rows the same way the
/// grid read them.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server_version: Option<String>,
pub table_meta: DataGridTableMeta,
pub columns: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -3939,6 +3944,7 @@ mod tests {
DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -4174,11 +4180,87 @@ mod tests {
assert_eq!(build_data_grid_conditional_update_sql(primary_key), None);
}
#[test]
fn neo4j_data_grid_save_addresses_nodes_with_the_identity_function_of_the_server() {
let options = |server_version: Option<&str>, rows: Vec<Vec<Value>>| DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Neo4j),
identifier_quote: None,
server_version: server_version.map(str::to_string),
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: None,
table_name: "Employee".to_string(),
primary_keys: vec![],
columns: Some(vec![
column(DBX_NEO4J_ELEMENT_ID_COLUMN, "string", false, None),
column("name", "string", true, None),
]),
},
columns: vec![DBX_NEO4J_ELEMENT_ID_COLUMN.to_string(), "name".to_string()],
source_columns: None,
rows,
dirty_rows: vec![(0, vec![(1, json!("after"))])],
deleted_rows: vec![1],
new_rows: vec![],
include_database_name: false,
};
let legacy_rows = || vec![vec![json!(23), json!("before")], vec![json!(24), json!("gone")]];
// Neo4j 4.x only knows `id()`; `elementId()` would be an unknown function there (#10503).
// That `id()` is an Integer while grids carry text, so the predicate compares numbers —
// with a quoted literal the edit would silently match no row.
for rows in [legacy_rows(), vec![vec![json!("23"), json!("before")], vec![json!("24"), json!("gone")]]] {
let on_neo4j_4 = prepare_data_grid_save(options(Some("Neo4j/4.4.44"), rows));
assert_eq!(
on_neo4j_4.statements,
vec![
"MATCH (n:`Employee`) WHERE id(n) = 23 SET n.`name` = 'after';".to_string(),
"MATCH (n:`Employee`) WHERE id(n) = 24 DETACH DELETE n;".to_string(),
]
);
}
// Neo4j 5.0+ renamed the identity function, and `elementId()` returns a string.
let on_neo4j_5 = prepare_data_grid_save(options(
Some("Neo4j/5.26.0"),
vec![vec![json!("4:0db5d0e9:1:6"), json!("before")], vec![json!("4:0db5d0e9:1:7"), json!("gone")]],
));
assert_eq!(
on_neo4j_5.statements,
vec![
"MATCH (n:`Employee`) WHERE elementId(n) = '4:0db5d0e9:1:6' SET n.`name` = 'after';".to_string(),
"MATCH (n:`Employee`) WHERE elementId(n) = '4:0db5d0e9:1:7' DETACH DELETE n;".to_string(),
]
);
// A non-numeric identity never becomes a bare Cypher number, even on the legacy function.
let legacy_text_id = prepare_data_grid_save(options(
Some("Neo4j/4.4.44"),
vec![vec![json!("4:0db5d0e9:1:6"), json!("before")], vec![json!("4:0db5d0e9:1:7"), json!("gone")]],
));
assert_eq!(
legacy_text_id.statements,
vec![
"MATCH (n:`Employee`) WHERE id(n) = '4:0db5d0e9:1:6' SET n.`name` = 'after';".to_string(),
"MATCH (n:`Employee`) WHERE id(n) = '4:0db5d0e9:1:7' DETACH DELETE n;".to_string(),
]
);
// Callers without connection metadata keep the historical `elementId()` spelling.
let without_version = prepare_data_grid_save(options(
None,
vec![vec![json!("4:0db5d0e9:1:6"), json!("before")], vec![json!("4:0db5d0e9:1:7"), json!("gone")]],
));
assert_eq!(without_version.statements, on_neo4j_5.statements);
}
#[test]
fn iris_cache_data_grid_save_uses_unquoted_ordinary_identifiers() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Iris),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -4216,6 +4298,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Iris),
identifier_quote: Some("\"".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -4259,6 +4342,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -4296,6 +4380,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6363,6 +6448,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Hive),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6403,6 +6489,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Hive),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6442,6 +6529,7 @@ mod tests {
let hive_options = DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Hive),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6464,6 +6552,7 @@ mod tests {
let postgres_options = DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6647,6 +6736,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: Some("\"".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6714,6 +6804,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: Some("\"".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6749,6 +6840,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6775,6 +6867,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: Some("BarDB".to_string()),
database: Some("BarDB".to_string()),
@@ -6805,6 +6898,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6843,6 +6937,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6878,6 +6973,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6913,6 +7009,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -6943,6 +7040,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7020,6 +7118,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Vastbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7051,6 +7150,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Vastbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7094,6 +7194,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Goldendb),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7126,6 +7227,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Goldendb),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7166,6 +7268,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Goldendb),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7193,6 +7296,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Goldendb),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7220,6 +7324,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7256,6 +7361,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7287,6 +7393,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: Some("`".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7320,6 +7427,7 @@ mod tests {
let googlesql = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Spanner),
identifier_quote: Some("`".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7344,6 +7452,7 @@ mod tests {
let postgres_dialect = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Spanner),
identifier_quote: Some("\"".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7375,6 +7484,7 @@ mod tests {
let no_reported_quote = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Spanner),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7453,6 +7563,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Vastbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: Some("smes_dev".to_string()),
@@ -7487,6 +7598,7 @@ mod tests {
DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Informix),
identifier_quote: Some(String::new()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: Some("webcenter".to_string()),
@@ -7531,6 +7643,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Informix),
identifier_quote: Some(String::new()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: Some("dbx_test".to_string()),
@@ -7566,6 +7679,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Gaussdb),
identifier_quote: Some("\"".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7592,6 +7706,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: Some("`".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7621,6 +7736,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7659,6 +7775,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7695,6 +7812,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7729,6 +7847,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7763,6 +7882,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7804,6 +7924,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::OceanbaseOracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7851,6 +7972,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::OceanbaseOracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7882,6 +8004,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::OceanbaseOracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -7915,6 +8038,7 @@ mod tests {
DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Dameng),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8054,6 +8178,7 @@ mod tests {
DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Sqlite),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8213,6 +8338,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::OceanbaseOracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8365,6 +8491,7 @@ mod tests {
let options = DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8420,6 +8547,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8516,6 +8644,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: Some("`".to_string()),
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8608,6 +8737,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Dameng),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8644,6 +8774,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8694,6 +8825,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8762,6 +8894,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8796,6 +8929,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8828,6 +8962,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8875,6 +9010,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8905,6 +9041,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8935,6 +9072,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::SqlServer),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -8967,6 +9105,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9000,6 +9139,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9042,6 +9182,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9075,6 +9216,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9112,6 +9254,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9152,6 +9295,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9199,6 +9343,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9244,6 +9389,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9274,6 +9420,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9303,6 +9450,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9337,6 +9485,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Tdengine),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9368,6 +9517,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Databend),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9402,6 +9552,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::ClickHouse),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9438,6 +9589,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::ClickHouse),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9472,6 +9624,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::ClickHouse),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9502,6 +9655,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::ClickHouse),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9601,6 +9755,7 @@ mod tests {
let save = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Doris),
identifier_quote: None,
server_version: None,
table_meta,
columns: vec!["id".to_string(), "status".to_string()],
source_columns: None,
@@ -9626,6 +9781,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Databend),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9657,6 +9813,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oscar),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9688,6 +9845,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9724,6 +9882,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9778,6 +9937,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9806,6 +9966,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9840,6 +10001,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::ManticoreSearch),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9873,6 +10035,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Postgres),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9913,6 +10076,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Oracle),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9949,6 +10113,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Xugu),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -9986,6 +10151,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Sqlite),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10015,6 +10181,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Sqlite),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10047,6 +10214,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10076,6 +10244,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10107,6 +10276,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10137,6 +10307,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10168,6 +10339,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Mysql),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10197,6 +10369,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10226,6 +10399,7 @@ mod tests {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Sqlite),
identifier_quote: None,
server_version: None,
table_meta: DataGridTableMeta {
catalog: None,
database: None,
@@ -10266,6 +10440,7 @@ mod tests {
let save = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Doris),
identifier_quote: None,
server_version: None,
table_meta: table_meta.clone(),
columns: vec!["name".to_string(), "v2".to_string()],
source_columns: None,
+3 -2
View File
@@ -47,8 +47,9 @@ pub use table_select::{
build_table_select_sql, DBX_LARGE_VALUE_BYTES_COLUMN_PREFIX,
};
pub use table_select::{
database_qualified_table_name, quote_table_data_identifier, table_data_qualified_table_name, table_data_schema,
uses_connection_identifier_quote,
database_qualified_table_name, neo4j_element_id_function, quote_table_data_identifier,
table_data_qualified_table_name, table_data_schema, uses_connection_identifier_quote,
NEO4J_LEGACY_ELEMENT_ID_FUNCTION,
};
pub use type_rewrite::{
apply_auto_inc_to_column_def, column_is_auto_increment, normalize_len_params, rewrite_column_type,
@@ -873,6 +873,38 @@ pub(super) fn build_db2_table_select_page_sql(
)
}
/// Neo4j 5.0 replaced `id()` with `elementId()`, and servers before that only know `id()`, so the
/// generated Cypher has to pick the spelling the connected server accepts. An unknown version keeps
/// `elementId()`, which is what every caller without connection metadata got before.
pub fn neo4j_element_id_function(server_version: Option<&str>) -> &'static str {
match neo4j_major_version(server_version) {
Some(major) if major < NEO4J_ELEMENT_ID_MIN_MAJOR_VERSION => NEO4J_LEGACY_ELEMENT_ID_FUNCTION,
_ => "elementId",
}
}
/// The identity function Neo4j used before 5.0. Unlike `elementId()`, which returns a string, it
/// returns the node's internal `Integer` id, so callers comparing a value read from a grid have to
/// compare numbers.
pub const NEO4J_LEGACY_ELEMENT_ID_FUNCTION: &str = "id";
/// Extracts the leading `major` from version strings such as `4.4.44`, `Neo4j/5.26.0` or
/// `Neo4j/2025.01.0`. The product name itself contains a digit ("Neo4j"), so only a token that
/// *starts* with digits counts as the version. Anything without one is treated as unknown.
fn neo4j_major_version(server_version: Option<&str>) -> Option<u32> {
let version = server_version?.trim();
version.split(|character: char| !character.is_ascii_alphanumeric()).find_map(|token| {
let digits: String = token.chars().take_while(char::is_ascii_digit).collect();
if digits.is_empty() {
None
} else {
digits.parse().ok()
}
})
}
const NEO4J_ELEMENT_ID_MIN_MAJOR_VERSION: u32 = 5;
pub(super) fn build_neo4j_table_select_sql(options: &TableDataSelectSqlOptions, limit: usize) -> String {
let label = quote_table_identifier(Some(DatabaseType::Neo4j), &options.table_name);
let predicate = normalize_where_input(options.where_input.as_deref());
@@ -891,7 +923,8 @@ pub(super) fn build_neo4j_table_select_sql(options: &TableDataSelectSqlOptions,
.join(", ")
};
let returns = format!(
"elementId(n) AS {}, {returned_columns}",
"{}(n) AS {}, {returned_columns}",
neo4j_element_id_function(options.server_version.as_deref()),
quote_table_identifier(Some(DatabaseType::Neo4j), DBX_NEO4J_ELEMENT_ID_COLUMN)
);
let order_by = options.order_by.as_deref().filter(|order| !order.trim().is_empty());
@@ -979,6 +1012,7 @@ mod tests {
database_type: Some(database_type),
driver_profile: None,
identifier_quote: None,
server_version: None,
schema: None,
table_name: table.to_string(),
catalog: catalog.map(|c| c.to_string()),
@@ -1575,6 +1575,37 @@ fn builds_oracle_and_neo4j_table_data_queries() {
);
}
#[test]
fn neo4j_element_id_function_follows_the_connected_server_version() {
// Neo4j 4.x and older only know `id()`; `elementId()` arrived in 5.0.
assert_eq!(neo4j_element_id_function(Some("4.4.44")), "id");
assert_eq!(neo4j_element_id_function(Some("Neo4j/4.4.44")), "id");
assert_eq!(neo4j_element_id_function(Some("Neo4j 4.4.44 (community)")), "id");
assert_eq!(neo4j_element_id_function(Some("3.5.35")), "id");
assert_eq!(neo4j_element_id_function(Some("Neo4j/5.26.0")), "elementId");
assert_eq!(neo4j_element_id_function(Some("Neo4j/2025.01.0")), "elementId");
// Unknown or unparsable versions keep the current spelling.
assert_eq!(neo4j_element_id_function(None), "elementId");
assert_eq!(neo4j_element_id_function(Some("")), "elementId");
assert_eq!(neo4j_element_id_function(Some("Neo4j Community")), "elementId");
}
#[test]
fn builds_neo4j_table_select_with_the_legacy_element_id_on_neo4j_4() {
assert_eq!(
build_table_data_select_sql(TableDataSelectSqlOptions {
database_type: Some(DatabaseType::Neo4j),
table_name: "Employee".to_string(),
primary_keys: vec!["id".to_string()],
columns: vec!["id".to_string(), "role".to_string()],
server_version: Some("Neo4j/4.4.44".to_string()),
limit: Some(100),
..Default::default()
}),
"MATCH (n:`Employee`) RETURN id(n) AS `__DBX_ELEMENT_ID`, n.`id` AS `id`, n.`role` AS `role` LIMIT 100;"
);
}
#[test]
fn oracle_unknown_table_type_does_not_assume_rowid_support() {
assert_eq!(
@@ -25,6 +25,11 @@ pub struct TableDataSelectSqlOptions {
pub driver_profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier_quote: Option<String>,
/// Server version reported by the connection (for example `Neo4j/4.4.44`).
/// Engines that renamed a built-in function across releases use it to emit
/// SQL the connected server understands instead of the newest spelling.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub table_name: String,