{"record":{"id":"708435d34b5f0209","repo":"siyuan-note/siyuan","slug":"sql-value-must-be-a-json-scalar","errorCode":null,"errorMessage":"SQL value must be a JSON scalar","messagePattern":"SQL value must be a JSON scalar","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/query.go","lineNumber":27,"sourceCode":"type SQLQueryRequest struct {\n\tStmt string `json:\"stmt\" api:\"trim\"`\n\tMode string `json:\"mode\" api:\"optional,nullable\"`\n}\n\n// SQLValue 保留数据库标量的 JSON 表示，包括整数精度和二进制值的 Base64 文本。\ntype SQLValue struct{ raw json.RawMessage }\n\nfunc (v SQLValue) MarshalJSON() ([]byte, error) {\n\tif len(v.raw) == 0 {\n\t\treturn []byte(\"null\"), nil\n\t}\n\treturn v.raw, nil\n}\n\nfunc (v *SQLValue) UnmarshalJSON(data []byte) error {\n\tdata = bytes.TrimSpace(data)\n\tif !json.Valid(data) || data[0] == '{' || data[0] == '[' {\n\t\treturn fmt.Errorf(\"SQL value must be a JSON scalar\")\n\t}\n\tv.raw = append(v.raw[:0], data...)\n\treturn nil\n}\n\ntype SQLRows []map[string]SQLValue\n\ntype SQLQueryLimit struct {\n\tLimit     int  `json:\"limit\"`\n\tTruncated bool `json:\"truncated\"`\n}\n\n// SuccessSQL 将限额信息保留在信封顶层，查询结果仍位于 data。\nfunc SuccessSQL(rows SQLRows, limit int, truncated bool) Response[SQLRows] {\n\treturn Response[SQLRows]{data: rows, queryLimit: &SQLQueryLimit{Limit: limit, Truncated: truncated}}\n}\n","sourceCodeStart":9,"sourceCodeEnd":44,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/query.go#L9-L44","documentation":"SQLValue wraps a single cell of a SQL query result and only accepts JSON scalars (numbers, strings, booleans, null). Its UnmarshalJSON trims whitespace and rejects input that is not valid JSON or that starts with '{' or '[', returning \"SQL value must be a JSON scalar\". Rows are represented as map[string]SQLValue, so any object or array in the data stream violates the row contract and must be decoded differently.","triggerScenarios":"json.Unmarshal / json.Decode into SQLValue or SQLRows ([]map[string]SQLValue) when a cell contains a JSON object or array — e.g. row data produced by marshaling map[string]any where a column holds a nested structure, or a query result embedding JSON documents in a column.","commonSituations":"A SQLite column stores a JSON blob and the query returns it as an object; the producer marshals struct fields that contain slices/maps into each cell instead of scalars; an upstream API nests objects per cell; a test fixture builds rows with array values.","solutions":["Flatten nested values into scalar columns (stringified JSON, separate columns, or individual fields) before unmarshaling into SQLRows","Change the cell producer so each column emits a scalar (number, string, bool, or null)","If the value must stay structured, decode the row into map[string]json.RawMessage or a dedicated struct instead of SQLRows","Inspect the offending row/cell (first '{' or '[') and adjust the query with json_extract to pull out scalars"],"exampleFix":"// before\nrows := SQLRows{{\"meta\": mustJSON(map[string]any{\"a\": 1})}} // cell starts with '{'\n// after\nrows := SQLRows{{\"meta\": rawMessage(\"\\\"{\\\\\\\"a\\\\\\\":1}\\\"\")}} // scalar string, or split into scalar columns","handlingStrategy":"type-guard","validationCode":"func isScalarCell(v any) bool {\n  switch t := v.(type) {\n  case nil, string, bool, float64, int64, int: return true\n  default: return false\n  }\n}","typeGuard":"func isJSONScalar(raw []byte) bool {\n  d := bytes.TrimSpace(raw)\n  return len(d) > 0 && json.Valid(d) && d[0] != '{' && d[0] != '['\n}","tryCatchPattern":"var v SQLValue\nif err := json.Unmarshal(cell, &v); err != nil { if strings.Contains(err.Error(), \"SQL value must be a JSON scalar\") { return flattenCell(cell) }; return err }","preventionTips":["Keep SQL row cells scalar: use json_extract in queries for JSON columns","Never marshal slices/maps/structs directly into a cell value","Decode structured rows into typed structs or map[string]json.RawMessage instead of SQLRows","Add fixture tests that include nested values to catch this at decode time"],"tags":["json","sql","unmarshal","type-mismatch"],"backgroundTag":"type-mismatch","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}