对Firebase Data Connect的嵌套SQL插入(使用 _execute)出现不明确的错误
我需要在Firebase Data Connect数据库中一次性向多个表插入数据,使用 原生SQL。当我从云函数调用mutation时失败,Data Connect总是抛出同样的 Invalid SQL statement 错误,且没有更多信息,这让定位原因变得非常困难。
我找不到关于如何进一步调试这个Firebase Data Connect错误的文档。
将每条插入分开提取并通过应用程序逐条调用标准 mutations.gql, 进行测试时,一切正常;然而在云函数中将插入组合成一个嵌套的SQL语句进行调用时却不行,我只得到 "Invalid SQL statement" 的错误,尽管每条插入单独执行时的SQL和输入数据都肯定是正确的。
还有谁有在Firebase Data Connect中使用嵌套原生SQL插入的经验,或知道如何进一步调试这样的语句?
以下是我的云函数:
await dataConnect.executeGraphql(
`
mutation InsertRecipe(
$param1: String!,
$param2: String,
$param3: String!,
$param4: String!,
$param5: [String!]!,
$param6: String!,
$param7: Int!,
$param8: Boolean!
) {
_execute(
sql: """
WITH ensure_user AS (
INSERT INTO "User" (id, displayName)
VALUES ($2, null)
ON CONFLICT (id) DO NOTHING
),
new_recipe AS (
INSERT INTO Recipe (
title,
authorId,
subtitle,
description,
foodTypes,
imageUrl,
estimatedMinutes,
isTest
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
RETURNING id
)
SELECT id FROM new_recipe;
""",
params: [
$param1,
$param2,
$param3,
$param4,
$param5,
$param6,
$param7,
$param8
]
)
}
`,
{
variables: {
param1: draft.param1,
param2: draft.param2,
param3: draft.param3,
param4: draft.param4,
param5: draft.param5,
param6: param6,
param7: draft.param7,
param8: draft.param8,
},
}
);
Edit: 当移除 SELECT id FROM new_recipe 时—正如Pedro正确提到的—同样的错误仍然会被抛出。
我尝试了不同的表名,例如 user、User、~~"user"~~、"User" 等等。[编辑:原来除了 "user" 之外我都尝试过,它才是正确的名称],但错误始终相同,我不知道原因。像 SELECT 1; 这样的简单查询是可用的,因此访问Data Connect不是问题,我相信这本来会导致不同的错误。
数据格式错误或空/未定义的值也不是原因,因为同样的单独查询在应用中直接调用时使用相同的输入数据是可工作的,只有嵌套调用不行。
所以问题要么是:
- Data Connect不以我当前的方式支持原生SQL,或者
- 使用
executeGraphgl设置并调用变更的方式并不像我这样做的那样工作。
然而错误信息和文档没有给我更多信息。
以下是错误信息:
FirebaseDataConnectError: Invalid SQL statement
at DataConnectApiClient.makeGqlRequest (/workspace/node_modules/firebase-admin/lib/data-connect/data-connect-api-client-internal.js:270:19)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
at async DataConnectApiClient.executeGraphqlHelper (/workspace/node_modules/firebase-admin/lib/data-connect/data-connect-api-client-internal.js:129:26) {
errorInfo: {
code: 'data-connect/query-error',
message: 'Invalid SQL statement'
},
codePrefix: 'data-connect'
}
当把 _execute SQL语句替换为gql mutation时,查询按预期工作:
await dataConnect.executeGraphql(
`
mutation InsertRecipe(
$param1: String!,
$param2: String,
$param3: String!,
$param4: String!,
$param5: [String!]!,
$param6: String!,
$param7: Int!,
$param8: Boolean!
) {
recipe_insert(data: {
title: $param1
authorId: $param2
subtitle: $param3
description: $param4
foodTypes: $param5
imageUrl: $param6
estimatedMinutes: $param7
isTest: $param8
})
}
`,
{
variables: {
param1: draft.param1,
param2: draft.param2,
param3: draft.param3,
param4: draft.param4,
param5: draft.param5,
param6: param6,
param7: draft.param7,
param8: draft.param8,
},
}
);
解决方案
我是在Data Connect团队的Firebaser。这是一个很好的问题。你实际上遇到了一些在同一时间叠加的约束,在编写原生SQL时这是一个非常常见的坑。让我们逐步拆解到底发生了什么。
1.架构大小写与类型转换
你查询最初失败的主要原因在于Data Connect将 GraphQL映射到Postgres的方式。即使你在GraphQL架构中定义了诸如 User 和 authorId 这样的字段,Data Connect会在底层Postgres自动创建表和列,名称是snake_case("user" 和 author_id)。
当你进入原生SQL时,必须使用那些实际的数据库名称,而不是GraphQL的 camelCase名称。关于此映射的更多信息,可以参考 Data Connect Schemas Guide。正如 @Pedro提到的,_execute 操作不支持尾随 RETURNING 或 SELECT 子句。数据库会仍然执行该语句,但不会将预期的数据返回给客户端——你只会得到标准的 _execute 确认。尽管如此,这不会导致错误。
2.为什么SDK会隐藏真实错误
你提到只看到通用错误信息,这让调试变得非常沮丧。我们的后端实际上捕捉到了确切的语法错误,但我们生成的所有SDK(包括 firebase-admin)都故意屏蔽数据库级错误。这是一种安全最佳实践(CWE-209),以确保你的应用不会无意中向客户端运行时泄露内部数据库架构细节。
如何看到真实错误:
要看到PostgreSQL的确切报错,你需要绕过SDK,直接查看后端日志:
- 本地:查看在你的项目文件夹中由模拟器生成的
dataconnect-debug.log文件。 -
生产环境:在Google Cloud控制台中为Data Connect服务查看Google Cloud日志(Logs Explorer)。
-
CTE和
SELECT的变通方法
正如Pedro正确指出的,execute严格执行数据修改语言(DML)操作,只会忽略任何尾随的SELECT语句。此外,如我们在 原生SQL文档 中所述,数据修改型的CTE(如在WITH块中的INSERT)仅由execute支持,不被executeReturning支持。
基于这些规则,你不能在单个参数化请求中运行一个复杂的、会修改数据的CTE并返回ID。
一种替代方案是把你的CTE包装在一个返回ID的自定义Postgres函数中,然后可以通过_executeReturning* 调用。
在你的场景中,最干净的解决办法是将逻辑拆分成两条顺序查询,在你的Node函数中执行:
JavaScript
// Step 1: Perform the inserts using _execute (Note the snake_case and ::uuid casts)
await dataConnect.executeGraphql(`
mutation InsertRecipeCTE(
$param1: String!
$param2: String!
$param3: String!
$param4: String!
$param6: String!
$param7: Int!
$param8: Boolean!
) {
_execute(
sql: """
WITH ensure_user AS (
INSERT INTO "user" (id, display_name)
VALUES ($2::uuid, 'Default Name')
ON CONFLICT (id) DO NOTHING
)
INSERT INTO "recipe" (
title, author_id, subtitle, description, image_url, estimated_minutes, is_test
)
VALUES ($1, $2::uuid, $3, $4, $5, $6, $7);
""",
params: [$param1, $param2, $param3, $param4, $param6, $param7, $param8]
)
}
`, variables);
// Step 2: Fetch the generated ID using a separate _select query
const result = await dataConnect.executeGraphql(`
query GetRecipeId($title: String!, $authorId: String!) {
_select(
sql: """
SELECT id FROM "recipe"
WHERE title = $1 AND author_id = $2::uuid
""",
params: [$title, $authorId]
)
}
`, { variables: { title: variables.param1, authorId: variables.param2 } });
const recipeId = result.data._select[0].id;
希望这能帮助你清理原生SQL的调试流程!