{"record":{"id":"6b66bf10ab3341b5","repo":"dotnet/orleans","slug":"the-name-must-be-a-legal-sql-table-name","errorCode":null,"errorMessage":"The name must be a legal SQL table name","messagePattern":"The name must be a legal SQL table name","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/AdoNet/Shared/Storage/RelationalStorageExtensions.cs","lineNumber":57,"sourceCode":"        private const string indexedParameterTemplate = \"@p{0}\";\n\n        /// <summary>\n        /// Executes a multi-record insert query clause with <em>SELECT UNION ALL</em>.\n        /// </summary>\n        /// <typeparam name=\"T\"></typeparam>\n        /// <param name=\"storage\">The storage to use.</param>\n        /// <param name=\"tableName\">The table name to against which to execute the query.</param>\n        /// <param name=\"parameters\">The parameters to insert.</param>\n        /// <param name=\"nameMap\">If provided, maps property names from <typeparamref name=\"T\"/> to ones provided in the map.</param>\n        /// <param name=\"onlyOnceColumns\">If given, SQL parameter values for the given <typeparamref name=\"T\"/> property types are generated only once. Effective only when <paramref name=\"useSqlParams\"/> is <em>TRUE</em>.</param>\n        /// <param name=\"useSqlParams\"><em>TRUE</em> if the query should be in parameterized form. <em>FALSE</em> otherwise.</param>\n        /// <param name=\"cancellationToken\">The cancellation token. Defaults to <see cref=\"CancellationToken.None\"/>.</param>\n        /// <returns>The rows affected.</returns>\n        public static Task<int> ExecuteMultipleInsertIntoAsync<T>(this IRelationalStorage storage, string tableName, IEnumerable<T> parameters, IReadOnlyDictionary<string, string>? nameMap = null, IEnumerable<string>? onlyOnceColumns = null, bool useSqlParams = true, CancellationToken cancellationToken = default)\n        {\n            if(string.IsNullOrWhiteSpace(tableName))\n            {\n                throw new ArgumentException(\"The name must be a legal SQL table name\", nameof(tableName));\n            }\n\n            if(parameters == null)\n            {\n                throw new ArgumentNullException(nameof(parameters));\n            }\n\n            var storageConsts = DbConstantsStore.GetDbConstants(storage.InvariantName);\n\n            var startEscapeIndicator = storageConsts.StartEscapeIndicator;\n            var endEscapeIndicator = storageConsts.EndEscapeIndicator;\n\n            //SqlParameters map is needed in case the query needs to be parameterized in order to avoid two\n            //reflection passes as first a query needs to be constructed and after that when a database\n            //command object has been created, parameters need to be provided to them.\n            var sqlParameters = new Dictionary<string, object?>();\n            const string insertIntoValuesTemplate = \"INSERT INTO {0} ({1}) SELECT {2};\";\n            var columns = string.Empty;","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/src/AdoNet/Shared/Storage/RelationalStorageExtensions.cs#L39-L75","documentation":"Thrown by RelationalStorageExtensions.ExecuteMultipleInsertIntoAsync<T> when tableName is null, empty, or whitespace. The table name is interpolated directly into the 'INSERT INTO {0} (...)' template, so a blank value would produce invalid SQL; the extension rejects it early with ArgumentException(nameof(tableName)).","triggerScenarios":"Calling storage.ExecuteMultipleInsertIntoAsync(\"\", parameters) or with a null table name; typical when the table name is computed from a grain state type name or read from options that were not bound.","commonSituations":"Naming convention refactor that yields empty for some state types; options binding skipped so the table name option is null; per-grain storage where a mapping function returns empty for a new grain type.","solutions":["Pass a concrete, validated table name matching your schema (e.g. 'GrainState' or 'OrleansMembershipTable').","Resolve table names from a mapping/registry and assert non-blank at startup.","Use the storage-constant escape indicators correctly; the name is wrapped in start/end escape indicators by the extension.","Add an integration test that inserts into each configured table to catch blank names early."],"exampleFix":"// before\nawait storage.ExecuteMultipleInsertIntoAsync(\"\", rows);\n\n// after\nconst string table = \"GrainState\";\nif (string.IsNullOrWhiteSpace(table)) throw new InvalidOperationException();\nawait storage.ExecuteMultipleInsertIntoAsync(table, rows);","handlingStrategy":"validation","validationCode":"if (string.IsNullOrWhiteSpace(tableName))\n    throw new InvalidOperationException(\"Table name is not configured.\");","typeGuard":"static bool IsValidTableName(string? s) => !string.IsNullOrWhiteSpace(s);","tryCatchPattern":"try { await storage.ExecuteMultipleInsertIntoAsync(table, rows); }\ncatch (ArgumentException ex) when (ex.ParamName == nameof(table)) { /* set table */ }","preventionTips":["Resolve table names from a registry and assert non-blank at startup.","Cover each grain-state type with an integration test.","Avoid computing table names from values that can be empty."],"tags":["csharp","dotnet","orleans","adonet","argument-validation"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}