{"record":{"id":"a99910b1bbfe32ca","repo":"Tencent/APIJSON","slug":"partition-value-item-value","errorCode":null,"errorMessage":"预编译模式下 @partition:value 中 ${item} 不合法! value 里面用 , 分割的每一项必须是 column 且其中 column 必须是 英语单词！并且不要有多余的空格！","messagePattern":"预编译模式下 @partition:value 中 (.+?) 不合法! value 里面用 , 分割的每一项必须是 column 且其中 column 必须是 英语单词！并且不要有多余的空格！","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java","lineNumber":1990,"sourceCode":"\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tString partition = StringUtil.trim(getPartition());\n\n\t\tString[] keys = StringUtil.split(partition);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinPartition, true) ? \"\" : (hasPrefix ? \" PARTITION BY \" : \"\") + joinPartition;\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tString item = keys[i];\n\t\t\tString origin = item;\n\n\t\t\tif (isPrepared()) { //不能通过 ? 来代替，SELECT 'id','name' 返回的就是 id:\"id\", name:\"name\"，而不是数据库里的值！\n\t\t\t\t//这里既不对origin trim，也不对 ASC/DESC ignoreCase，希望前端严格传没有任何空格的字符串过来，减少传输数据量，节约服务器性能\n\t\t\t\tif (StringUtil.isName(origin) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"预编译模式下 @partition:value 中 \" + item + \" 不合法! value 里面用 , 分割的\"\n\t\t\t\t\t\t\t+ \"每一项必须是 column 且其中 column 必须是 英语单词！并且不要有多余的空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(origin);\n\t\t}\n\n\t\treturn (hasPrefix ? \" PARTITION BY \" : \"\") + StringUtil.concat(StringUtil.get(keys), joinPartition, \", \");\n\t}\n\n\t@Override\n\tpublic String getFill() {\n\t\treturn fill;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setFill(String... conditions) {\n\t\treturn setFill(StringUtil.get(conditions));\n\t}\n\t@Override","sourceCodeStart":1972,"sourceCodeEnd":2008,"githubUrl":"https://github.com/Tencent/APIJSON/blob/5284052872898eddc449a58f629e5c8d588b8e22/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java#L1972-L2008","documentation":"Prepared-mode validation of @partition:value: each comma-separated item must be a single-word identifier (StringUtil.isName). PARTITION BY items are inlined into SQL, so only clean names are accepted.","triggerScenarios":"\"@partition\": \"toYYYYMM(created_at)\" (function call), \"@partition\": \"part key\" — any item that is not one identifier word.","commonSituations":"ClickHouse-style partition expressions pasted verbatim; expecting @partition to accept functions like @column does; multi-word identifiers.","solutions":["Pass plain partition column names: \"@partition\": \"eventDate\"","Put function expressions in RAW_MAP and reference them with @raw","Check for stray spaces after commas before sending"],"exampleFix":"// before\n{\"@partition\": \"toYYYYMM(created_at)\"}\n// after\n{\"@raw\": \"@partition\", \"@partition\": \"ymPartition\"}  // ymPartition -> toYYYYMM(created_at) in backend RAW_MAP","handlingStrategy":"validation","validationCode":"for (const item of String(obj['@partition'] ?? '').split(',')) {\n  if (item && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(item)) throw new Error(`@partition item '${item}' must be a single word; use @raw for expressions`);\n}","typeGuard":"const isSingleIdentifier = s => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);","tryCatchPattern":"try { await api.get(req); } catch (e) { if (e.message.includes('@partition')) convertPartitionToRawToken(req); else throw e; }","preventionTips":["Pre-register partition expressions in RAW_MAP and send their tokens","Keep @partition limited to column pickers"],"tags":["apijson","partition","prepared-statement","clickhouse"],"backgroundTag":null,"analyzedSha":"5284052872898eddc449a58f629e5c8d588b8e22","analyzedAt":"2026-08-14T15:15:29.577Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}