{"record":{"id":"bea85978c17c6486","repo":"dotnet/efcore","slug":"fromsqlnoncomposable","errorCode":"FromSqlNonComposable","errorMessage":"'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.","messagePattern":"'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it\\. Consider calling 'AsEnumerable' after the method to perform the composition on the client side\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/EFCore.Relational/Query/QuerySqlGenerator.cs","lineNumber":567,"sourceCode":"    /// <summary>\n    ///     Checks whether a given SQL string is composable, i.e. can be embedded as a subquery within a\n    ///     larger SQL query.\n    /// </summary>\n    /// <param name=\"sql\">An SQL string to be checked for composability.</param>\n    /// <exception cref=\"InvalidOperationException\">The given SQL isn't composable.</exception>\n    protected virtual void CheckComposableSql(string sql)\n    {\n        var span = sql.AsSpan().TrimStart();\n\n        while (true)\n        {\n            // SQL -- comment\n            if (span.StartsWith(\"--\"))\n            {\n                var i = span.IndexOf('\\n');\n                span = i > 0\n                    ? span[(i + 1)..].TrimStart()\n                    : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);\n                continue;\n            }\n\n            // SQL /* */ comment\n            if (span.StartsWith(\"/*\"))\n            {\n                var i = span.IndexOf(\"*/\");\n                span = i > 0\n                    ? span[(i + 2)..].TrimStart()\n                    : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);\n                continue;\n            }\n\n            break;\n        }\n\n        CheckComposableSqlTrimmed(span);\n    }","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/dotnet/efcore/blob/dbf9771522148d61a2467854921bd5dc6f6e6916/src/EFCore.Relational/Query/QuerySqlGenerator.cs#L549-L585","documentation":"Thrown by CheckComposableSql when a FromSqlInterpolated/FromSqlRaw SQL string contains a SQL line comment ('--') that is never terminated by a newline. Because EF needs to embed the raw SQL as a subquery (it wraps it in '(...) AS t') to compose LINQ operators over it, it must verify the leading token is composable; an unterminated comment makes that impossible to determine. This only triggers when additional LINQ operators are composed over the FromSql query (forcing VisitFromSql/CheckComposableSql to run).","triggerScenarios":"Calling FromSqlRaw/FromSqlInterpolated with a string that begins with or contains a '--' comment with no trailing '\\n', then chaining a LINQ operator (e.g. .Where(), .Select(), .ToList()) so EF must treat it as a subquery. Example: context.Blogs.FromSqlRaw(\"-- my query SELECT * FROM Blogs\").ToList() with the comment lacking a final newline.","commonSituations":"Building SQL strings dynamically and concatenating a comment header without a trailing newline; reading SQL from a resource/external source where trailing whitespace was stripped; copy-pasting a commented query fragment; using string interpolation that drops the terminating newline.","solutions":["Ensure any '--' line comment is followed by a newline ('\\n') so EF can skip past it.","Remove the inline comment from the raw SQL string.","If the SQL is genuinely non-composable (e.g. a stored proc call), call AsEnumerable()/AsAsyncEnumerable() immediately after FromSql so composition happens client-side and CheckComposableSql is never invoked.","Refactor the comment into a C# comment above the FromSql call instead of inside the SQL literal."],"exampleFix":"// before\nvar q = db.Blogs.FromSqlRaw(\"-- top secret\\nSELECT * FROM Blogs\").Where(b => b.Id > 0);\n// the above throws only if the literal lacks a newline; fix by terminating the comment:\nvar q = db.Blogs.FromSqlRaw(\"-- top secret\\r\\nSELECT * FROM Blogs\").Where(b => b.Id > 0);\n// or move the note out of SQL:\n// top secret\nvar q = db.Blogs.FromSqlRaw(\"SELECT * FROM Blogs\").Where(b => b.Id > 0);","handlingStrategy":"validation","validationCode":"static bool IsComposableSql(string sql)\n{\n    var i = 0;\n    while (i < sql.Length)\n    {\n        if (sql[i] == '-' && i + 1 < sql.Length && sql[i + 1] == '-')\n        {\n            var nl = sql.IndexOf('\\n', i);\n            if (nl < 0) return false; // unterminated line comment\n            i = nl + 1;\n        }\n        else if (sql[i] == '/' && i + 1 < sql.Length && sql[i + 1] == '*')\n        {\n            var close = sql.IndexOf(\"*/\", i, StringComparison.Ordinal);\n            if (close < 0) return false; // unterminated block comment\n            i = close + 2;\n        }\n        else { i++; }\n    }\n    return true;\n}\n\nif (!IsComposableSql(rawSql)) rawSql += \"\\n\"; // or call AsEnumerable() instead","typeGuard":null,"tryCatchPattern":"try { var r = db.Blogs.FromSqlRaw(sql).Where(b => b.Id > 0).ToList(); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"non-composable SQL\"))\n{\n    // fall back to client-side composition\n    var r = db.Blogs.FromSqlRaw(sql).AsEnumerable().Where(b => b.Id > 0).ToList();\n}","preventionTips":["Never end a FromSql SQL string with a line comment; always terminate '--' with a newline.","Centralize raw-SQL construction in one helper that appends a trailing newline.","Unit-test raw SQL fragments for composability before passing them to FromSql.","Prefer FromSqlInterpolated over string concatenation to avoid accidental comment truncation."],"tags":["fromsql","raw-sql","composability","query-translation"],"analyzedSha":"dbf9771522148d61a2467854921bd5dc6f6e6916","analyzedAt":"2026-08-06T20:46:03.226Z","schemaVersion":2},"datasetVersion":"2026-08-07T02:17:10.218Z"}