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>