This is an automated email from the ASF dual-hosted git repository.

CurtHagenlocher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 4b2902ad8 fix(csharp/src/Drivers/BigQuery): harden metadata query 
identifier and pattern handling (#4757)
4b2902ad8 is described below

commit 4b2902ad80d372e93cfb68f84b62f4712e71da7d
Author: Curt Hagenlocher <[email protected]>
AuthorDate: Wed Sep 16 09:44:07 2026 -0700

    fix(csharp/src/Drivers/BigQuery): harden metadata query identifier and 
pattern handling (#4757)
    
    The metadata queries in `GetObjects` and `GetTableSchema` built
    INFORMATION_SCHEMA SQL by interpolating caller-supplied catalog,
    dataset, table, and column values and patterns directly into the query
    text, relying on a regex allowlist. That allowlist was in tension with
    the fact that some legitimate BigQuery identifier and pattern values
    (for example, table names containing Unicode letters, spaces, or the '%'
    wildcard) don't satisfy it.
    
    This change tightens identifier handling and removes the interpolation:
    
    - Every caller-supplied value across all six INFORMATION_SCHEMA queries
    is now bound as a query parameter rather than interpolated into the SQL
    text.
    - The catalog and dataset that name the view can't be parameters
    (BigQuery doesn't support binding identifiers), so those are still
    validated against an allowlist, now anchored with \A/\z instead of ^/$
    (since .NET's $ also matches before a trailing newline). Rejected values
    are no longer echoed into the exception message, since a rejected value
    could itself contain characters - including newlines and other control
    characters - that might forge or split log/error output if interpolated
    there.
    - Search patterns (used for catalog, dataset, table, and column name
    filters) are now handled correctly: BigQuery's LIKE evaluates '%' and
    '_' as wildcards once the pattern is bound as a parameter, whereas the
    old allowlist rejected any pattern containing '%', and separately
    rejected Unicode letters/spaces that BigQuery permits in identifiers.
    - Catalog and dataset patterns, which are matched client-side via
    PatternToRegEx, are aligned with the table/column patterns, which are
    matched server-side via LIKE: every literal character is now escaped
    (previously '.' matched any character and '[' threw), and matching is
    now case-sensitive in both paths, matching BigQuery's own case-sensitive
    dataset and table names.
    - EscapeLikePattern doubles backslashes so that user-supplied '\'
    characters remain literal under BigQuery's LIKE escaping, per the ADBC
    search pattern semantics (which give special meaning only to '%' and
    '_', with no other escaping supported).
    
    Breaking changes, all of them cases that were previously incorrect:
    
    - Catalog and dataset patterns now match case-sensitively.
    - Regex metacharacters in a catalog or dataset pattern are literal.
    - A catalog or dataset containing a character outside [a-zA-Z0-9_-] is
    rejected rather than truncated past the first invalid character.
    
    Co-authored-by: Copilot <[email protected]>
    Copilot-Session: 02844248-1442-4b59-9cc7-fffc21b698fa
---
 csharp/src/Drivers/BigQuery/BigQueryConnection.cs | 174 ++++++++++++++++------
 1 file changed, 128 insertions(+), 46 deletions(-)

diff --git a/csharp/src/Drivers/BigQuery/BigQueryConnection.cs 
b/csharp/src/Drivers/BigQuery/BigQueryConnection.cs
index bc8957f00..6a1cdbc1b 100644
--- a/csharp/src/Drivers/BigQuery/BigQueryConnection.cs
+++ b/csharp/src/Drivers/BigQuery/BigQueryConnection.cs
@@ -583,7 +583,7 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
 
                 foreach (string projectId in projectIds)
                 {
-                    if (Regex.IsMatch(projectId, catalogRegexp, 
RegexOptions.IgnoreCase))
+                    if (Regex.IsMatch(projectId, catalogRegexp))
                     {
                         catalogNameBuilder.Append(projectId);
 
@@ -641,7 +641,7 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
                 {
                     foreach (BigQueryDataset schema in schemas)
                     {
-                        if (Regex.IsMatch(schema.Reference.DatasetId, 
dbSchemaRegexp, RegexOptions.IgnoreCase))
+                        if (Regex.IsMatch(schema.Reference.DatasetId, 
dbSchemaRegexp))
                         {
                             
dbSchemaNameBuilder.Append(schema.Reference.DatasetId);
                             length++;
@@ -694,27 +694,34 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
                 int length = 0;
 
                 string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.TABLES",
-                    Sanitize(catalog), Sanitize(dbSchema));
+                    SanitizeIdentifier(catalog), SanitizeIdentifier(dbSchema));
+
+                List<BigQueryParameter> parameters = new 
List<BigQueryParameter>();
+                List<string> predicates = new List<string>();
 
                 if (tableNamePattern != null)
                 {
-                    query = string.Concat(query, string.Format(" WHERE 
table_name LIKE '{0}'", Sanitize(tableNamePattern)));
-                    if (tableTypes?.Count > 0)
-                    {
-                        IEnumerable<string> sanitizedTypes = 
tableTypes.Select(x => Sanitize(x));
-                        query = string.Concat(query, string.Format(" AND 
table_type IN ('{0}')", string.Join("', '", sanitizedTypes).ToUpper()));
-                    }
+                    predicates.Add("table_name LIKE @tableNamePattern");
+                    parameters.Add(new BigQueryParameter("tableNamePattern", 
BigQueryDbType.String, EscapeLikePattern(tableNamePattern)));
                 }
-                else
+
+                if (tableTypes?.Count > 0)
                 {
-                    if (tableTypes?.Count > 0)
+                    // IN UNNEST rather than IN, because BigQuery does not 
expand a single array
+                    // parameter into an IN list.
+                    predicates.Add("table_type IN UNNEST(@tableTypes)");
+                    parameters.Add(new BigQueryParameter("tableTypes", 
BigQueryDbType.Array, tableTypes.Select(x => x.ToUpperInvariant()).ToList())
                     {
-                        IEnumerable<string> sanitizedTypes = 
tableTypes.Select(x => Sanitize(x));
-                        query = string.Concat(query, string.Format(" WHERE 
table_type IN ('{0}')", string.Join("', '", sanitizedTypes).ToUpper()));
-                    }
+                        ArrayElementType = BigQueryDbType.String,
+                    });
                 }
 
-                BigQueryResults? result = ExecuteQuery(query, parameters: 
null);
+                if (predicates.Count > 0)
+                {
+                    query = string.Concat(query, " WHERE ", string.Join(" AND 
", predicates));
+                }
+
+                BigQueryResults? result = ExecuteQuery(query, parameters);
 
                 if (result != null)
                 {
@@ -800,15 +807,21 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
                 ArrowBuffer.BitmapBuilder nullBitmapBuffer = new 
ArrowBuffer.BitmapBuilder();
                 int length = 0;
 
-                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.COLUMNS WHERE table_name = '{2}'",
-                    Sanitize(catalog), Sanitize(dbSchema), Sanitize(table));
+                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.COLUMNS WHERE table_name = @tableName",
+                    SanitizeIdentifier(catalog), SanitizeIdentifier(dbSchema));
+
+                List<BigQueryParameter> parameters = new 
List<BigQueryParameter>
+                {
+                    new BigQueryParameter("tableName", BigQueryDbType.String, 
table),
+                };
 
                 if (columnNamePattern != null)
                 {
-                    query = string.Concat(query, string.Format("AND 
column_name LIKE '{0}'", Sanitize(columnNamePattern)));
+                    query = string.Concat(query, " AND column_name LIKE 
@columnNamePattern");
+                    parameters.Add(new BigQueryParameter("columnNamePattern", 
BigQueryDbType.String, EscapeLikePattern(columnNamePattern)));
                 }
 
-                BigQueryResults? result = ExecuteQuery(query, parameters: 
null);
+                BigQueryResults? result = ExecuteQuery(query, parameters);
 
                 if (result != null)
                 {
@@ -902,10 +915,13 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
                 ArrowBuffer.BitmapBuilder nullBitmapBuffer = new 
ArrowBuffer.BitmapBuilder();
                 int length = 0;
 
-                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name = '{2}'",
-                   Sanitize(catalog), Sanitize(dbSchema), Sanitize(table));
+                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name = @tableName",
+                   SanitizeIdentifier(catalog), SanitizeIdentifier(dbSchema));
 
-                BigQueryResults? result = ExecuteQuery(query, parameters: 
null);
+                BigQueryResults? result = ExecuteQuery(query, new 
List<BigQueryParameter>
+                {
+                    new BigQueryParameter("tableName", BigQueryDbType.String, 
table),
+                });
 
                 if (result != null)
                 {
@@ -966,12 +982,16 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
         {
             return this.TraceActivity(activity =>
             {
-                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_name = '{2}' AND 
constraint_name = '{3}' ORDER BY ordinal_position",
-               Sanitize(catalog), Sanitize(dbSchema), Sanitize(table), 
Sanitize(constraintName));
+                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE table_name = @tableName 
AND constraint_name = @constraintName ORDER BY ordinal_position",
+               SanitizeIdentifier(catalog), SanitizeIdentifier(dbSchema));
 
                 StringArray.Builder constraintColumnNamesBuilder = new 
StringArray.Builder();
 
-                BigQueryResults? result = ExecuteQuery(query, parameters: 
null);
+                BigQueryResults? result = ExecuteQuery(query, new 
List<BigQueryParameter>
+                {
+                    new BigQueryParameter("tableName", BigQueryDbType.String, 
table),
+                    new BigQueryParameter("constraintName", 
BigQueryDbType.String, constraintName),
+                });
 
                 if (result != null)
                 {
@@ -1001,10 +1021,13 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
                 ArrowBuffer.BitmapBuilder nullBitmapBuffer = new 
ArrowBuffer.BitmapBuilder();
                 int length = 0;
 
-                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE WHERE constraint_name = 
'{2}'",
-                   Sanitize(catalog), Sanitize(dbSchema), 
Sanitize(constraintName));
+                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE WHERE constraint_name = 
@constraintName",
+                   SanitizeIdentifier(catalog), SanitizeIdentifier(dbSchema));
 
-                BigQueryResults? result = ExecuteQuery(query, parameters: 
null);
+                BigQueryResults? result = ExecuteQuery(query, new 
List<BigQueryParameter>
+                {
+                    new BigQueryParameter("constraintName", 
BigQueryDbType.String, constraintName),
+                });
 
                 if (result != null)
                 {
@@ -1042,15 +1065,42 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
             });
         }
 
-        private string PatternToRegEx(string? pattern)
+        /// <summary>
+        /// Translates an ADBC search pattern into an equivalent regular 
expression, for the
+        /// catalog and dataset patterns that are matched client-side against 
listing results
+        /// rather than server-side by a SQL LIKE.
+        /// </summary>
+        /// <remarks>
+        /// Only "%" and "_" carry meaning in an ADBC search pattern; every 
other character is
+        /// literal, so each one is escaped rather than passed through to the 
regex engine. The
+        /// match is case-sensitive so that these patterns behave the same way 
as the table and
+        /// column patterns, which reach BigQuery as a case-sensitive LIKE.
+        /// </remarks>
+        internal static string PatternToRegEx(string? pattern)
         {
             if (pattern == null)
                 return ".*";
 
-            StringBuilder builder = new StringBuilder("(?i)^");
-            string convertedPattern = pattern.Replace("_", ".").Replace("%", 
".*");
-            builder.Append(convertedPattern);
-            builder.Append("$");
+            StringBuilder builder = new StringBuilder(@"\A");
+
+            foreach (char c in pattern)
+            {
+                switch (c)
+                {
+                    case '_':
+                        builder.Append('.');
+                        break;
+                    case '%':
+                        builder.Append(".*");
+                        break;
+                    default:
+                        builder.Append(Regex.Escape(c.ToString()));
+                        break;
+                }
+            }
+
+            // \z rather than $, which in .NET also matches before a trailing 
newline.
+            builder.Append(@"\z");
 
             return builder.ToString();
         }
@@ -1141,10 +1191,13 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
         {
             return this.TraceActivity(activity =>
             {
-                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.COLUMNS WHERE table_name = '{2}'",
-                Sanitize(catalog), Sanitize(dbSchema), Sanitize(tableName));
+                string query = string.Format("SELECT * FROM 
`{0}`.`{1}`.INFORMATION_SCHEMA.COLUMNS WHERE table_name = @tableName",
+                SanitizeIdentifier(catalog), SanitizeIdentifier(dbSchema));
 
-                BigQueryResults? result = ExecuteQuery(query, parameters: 
null);
+                BigQueryResults? result = ExecuteQuery(query, new 
List<BigQueryParameter>
+                {
+                    new BigQueryParameter("tableName", BigQueryDbType.String, 
tableName),
+                });
 
                 List<Field> fields = new List<Field>();
 
@@ -1344,25 +1397,54 @@ namespace Apache.Arrow.Adbc.Drivers.BigQuery
             this._fileActivityListener?.Dispose();
         }
 
-        private static Regex sanitizedInputRegex = new 
Regex("^[a-zA-Z0-9_-]+");
+        /// <summary>
+        /// Matches a complete BigQuery project or dataset identifier. The 
character class is the
+        /// union of what BigQuery permits in each: a project id allows 
lowercase letters, digits
+        /// and hyphens, a dataset id allows letters, digits and underscores. 
Both anchors are
+        /// required - \A and \z rather than ^ and $, because .NET's $ also 
matches immediately
+        /// before a trailing newline.
+        /// </summary>
+        private static readonly Regex s_identifierRegex = new 
Regex(@"\A[a-zA-Z0-9_-]+\z", RegexOptions.CultureInvariant);
 
-        private string Sanitize(string? input)
+        /// <summary>
+        /// Validates a value that is interpolated into a query as a quoted 
identifier.
+        /// </summary>
+        /// <remarks>
+        /// BigQuery query parameters cannot stand in for identifiers, so the 
catalog and dataset
+        /// that name an INFORMATION_SCHEMA view have to be embedded in the 
query text. They are
+        /// therefore checked against the full set of characters BigQuery 
allows in those names;
+        /// anything else - a backtick that would close the identifier 
especially - is rejected.
+        /// Every other caller-supplied value is passed as a query parameter 
instead.
+        /// </remarks>
+        internal static string SanitizeIdentifier(string? input)
         {
             if (string.IsNullOrEmpty(input))
                 return string.Empty;
 
-            bool isValidInput = sanitizedInputRegex.IsMatch(input);
-
-            if (isValidInput)
-            {
-                return input!;
-            }
-            else
+            if (!s_identifierRegex.IsMatch(input))
             {
-                throw new AdbcException($"{input} is invalid", 
AdbcStatusCode.InvalidArgument);
+                // The rejected value is deliberately omitted: it failed 
validation precisely
+                // because it may contain characters - newlines and other 
control characters
+                // among them - that would let it forge or split log/error 
output if embedded.
+                throw new AdbcException("catalog or dataset identifier is 
invalid", AdbcStatusCode.InvalidArgument);
             }
+
+            return input!;
         }
 
+        /// <summary>
+        /// Escapes an ADBC search pattern so that BigQuery's LIKE operator 
gives it the meaning
+        /// the ADBC specification defines.
+        /// </summary>
+        /// <remarks>
+        /// An ADBC search pattern gives special meaning to "%" (zero or more 
characters) and "_"
+        /// (exactly one character) and nothing else - the specification 
states that escaping is
+        /// not supported. BigQuery's LIKE additionally treats "\" as an 
escape character, so a
+        /// backslash is doubled here to keep it literal. The two wildcards 
are deliberately left
+        /// alone so they still reach LIKE as wildcards.
+        /// </remarks>
+        internal static string EscapeLikePattern(string pattern) => 
pattern.Replace("\\", "\\\\");
+
         /// <summary>
         /// Gets the access token from the token endpoint.
         /// </summary>

Reply via email to