blob: 52bcc8e0eee3fc1608a94a832854f8c16931add8 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Apache.Arrow.Adbc.Tracing;
using Apache.Arrow.Ipc;
using Apache.Arrow.Types;
using Google;
using Google.Api.Gax;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Bigquery.v2.Data;
using Google.Cloud.BigQuery.Storage.V1;
using Google.Cloud.BigQuery.V2;
using TableFieldSchema = Google.Apis.Bigquery.v2.Data.TableFieldSchema;
using TableSchema = Google.Apis.Bigquery.v2.Data.TableSchema;
namespace Apache.Arrow.Adbc.Drivers.BigQuery
{
/// <summary>
/// BigQuery-specific implementation of <see cref="AdbcStatement"/>
/// </summary>
class BigQueryStatement : TracingStatement, ITokenProtectedResource, IDisposable
{
readonly BigQueryConnection bigQueryConnection;
readonly CancellationRegistry cancellationRegistry;
public BigQueryStatement(BigQueryConnection bigQueryConnection) : base(bigQueryConnection)
{
if (bigQueryConnection == null) { throw new AdbcException($"{nameof(bigQueryConnection)} cannot be null", AdbcStatusCode.InvalidArgument); }
// pass on the handler since this isn't accessible publicly
UpdateToken = bigQueryConnection.UpdateToken;
this.bigQueryConnection = bigQueryConnection;
this.cancellationRegistry = new CancellationRegistry();
}
public Func<Task>? UpdateToken { get; set; }
internal Dictionary<string, string>? Options { get; set; }
private BigQueryClient Client => this.bigQueryConnection.Client ?? throw new AdbcException("Client cannot be null");
private GoogleCredential Credential => this.bigQueryConnection.Credential ?? throw new AdbcException("Credential cannot be null");
private int MaxRetryAttempts => this.bigQueryConnection.MaxRetryAttempts;
private int RetryDelayMs => this.bigQueryConnection.RetryDelayMs;
public override string AssemblyVersion => BigQueryUtils.BigQueryAssemblyVersion;
public override string AssemblyName => BigQueryUtils.BigQueryAssemblyName;
public override void SetOption(string key, string value)
{
if (Options == null)
{
Options = new Dictionary<string, string>();
}
Options[key] = value;
}
public override QueryResult ExecuteQuery()
{
return ExecuteQueryInternalAsync().GetAwaiter().GetResult();
}
private async Task<QueryResult> ExecuteQueryInternalAsync()
{
return await this.TraceActivityAsync(async activity =>
{
QueryOptions queryOptions = ValidateOptions(activity);
activity?.AddConditionalTag(SemanticConventions.Db.Query.Text, SqlQuery, this.bigQueryConnection.IsSafeToTrace);
BigQueryJob job = await Client.CreateQueryJobAsync(SqlQuery, null, queryOptions);
JobReference jobReference = job.Reference;
GetQueryResultsOptions getQueryResultsOptions = new GetQueryResultsOptions();
if (Options?.TryGetValue(BigQueryParameters.GetQueryResultsOptionsTimeout, out string? timeoutSeconds) == true &&
int.TryParse(timeoutSeconds, out int seconds) &&
seconds >= 0)
{
getQueryResultsOptions.Timeout = TimeSpan.FromSeconds(seconds);
activity?.AddBigQueryParameterTag(BigQueryParameters.GetQueryResultsOptionsTimeout, seconds);
}
using JobCancellationContext cancellationContext = new JobCancellationContext(cancellationRegistry, job);
// We can't checkJobStatus, Otherwise, the timeout in QueryResultsOptions is meaningless.
// When encountering a long-running job, it should be controlled by the timeout in the Google SDK instead of blocking in a while loop.
Func<Task<BigQueryResults>> getJobResults = async () =>
{
return await ExecuteCancellableJobAsync(cancellationContext, activity, async (context) =>
{
// if the authentication token was reset, then we need a new job with the latest token
context.Job = await Client.GetJobAsync(jobReference, cancellationToken: context.CancellationToken).ConfigureAwait(false);
return await context.Job.GetQueryResultsAsync(getQueryResultsOptions, cancellationToken: context.CancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
};
BigQueryResults results = await ExecuteWithRetriesAsync(getJobResults, activity, cancellationContext.CancellationToken).ConfigureAwait(false);
TokenProtectedReadClientManger clientMgr = new TokenProtectedReadClientManger(Credential);
clientMgr.UpdateToken = () => Task.Run(() =>
{
this.bigQueryConnection.SetCredential();
clientMgr.UpdateCredential(Credential);
});
// For multi-statement queries, StatementType == "SCRIPT"
if (results.TableReference == null || job.Statistics.Query.StatementType.Equals("SCRIPT", StringComparison.OrdinalIgnoreCase))
{
string statementType = string.Empty;
if (Options?.TryGetValue(BigQueryParameters.StatementType, out string? statementTypeString) == true)
{
statementType = statementTypeString;
}
int statementIndex = 1;
if (Options?.TryGetValue(BigQueryParameters.StatementIndex, out string? statementIndexString) == true &&
int.TryParse(statementIndexString, out int statementIndexInt) &&
statementIndexInt > 0)
{
statementIndex = statementIndexInt;
}
string evaluationKind = string.Empty;
if (Options?.TryGetValue(BigQueryParameters.EvaluationKind, out string? evaluationKindString) == true)
{
evaluationKind = evaluationKindString;
}
Func<Task<BigQueryResults>> getMultiJobResults = async () =>
{
// To get the results of all statements in a multi-statement query, enumerate the child jobs. Related public docs: https://cloud.google.com/bigquery/docs/multi-statement-queries#get_all_executed_statements.
// Can filter by StatementType and EvaluationKind. Related public docs: https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#jobstatistics2, https://cloud.google.com/bigquery/docs/reference/rest/v2/Job#evaluationkind
ListJobsOptions listJobsOptions = new ListJobsOptions();
listJobsOptions.ParentJobId = results.JobReference.JobId;
var joblist = Client.ListJobs(listJobsOptions)
.Select(job => Client.GetJob(job.Reference))
.Where(job => string.IsNullOrEmpty(evaluationKind) || job.Statistics.ScriptStatistics.EvaluationKind.Equals(evaluationKind, StringComparison.OrdinalIgnoreCase))
.Where(job => string.IsNullOrEmpty(statementType) || job.Statistics.Query.StatementType.Equals(statementType, StringComparison.OrdinalIgnoreCase))
.OrderBy(job => job.Resource.Statistics.CreationTime)
.ToList();
if (joblist.Count > 0)
{
if (statementIndex < 1 || statementIndex > joblist.Count)
{
throw new ArgumentOutOfRangeException($"The specified index {statementIndex} is out of range. There are {joblist.Count} jobs available.");
}
BigQueryJob indexedJob = joblist[statementIndex - 1];
cancellationContext.Job = indexedJob;
return await ExecuteCancellableJobAsync(cancellationContext, activity, async (context) =>
{
return await indexedJob.GetQueryResultsAsync(getQueryResultsOptions, cancellationToken: context.CancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
throw new AdbcException($"Unable to obtain result from statement [{statementIndex}]", AdbcStatusCode.InvalidData);
};
results = await ExecuteWithRetriesAsync(getMultiJobResults, activity, cancellationContext.CancellationToken).ConfigureAwait(false);
}
if (results?.TableReference == null)
{
throw new AdbcException("There is no query statement");
}
string table = $"projects/{results.TableReference.ProjectId}/datasets/{results.TableReference.DatasetId}/tables/{results.TableReference.TableId}";
int maxStreamCount = 1;
if (Options?.TryGetValue(BigQueryParameters.MaxFetchConcurrency, out string? maxStreamCountString) == true)
{
if (int.TryParse(maxStreamCountString, out int count))
{
if (count >= 0)
{
maxStreamCount = count;
}
}
}
long totalRows = results.TotalRows == null ? -1L : (long)results.TotalRows.Value;
Func<Task<IEnumerable<IArrowReader>>> getArrowReadersFunc = async () =>
{
return await ExecuteCancellableJobAsync(cancellationContext, activity, async (context) =>
{
// Cancelling this step may leave the server with unread streams.
return await GetArrowReaders(clientMgr, table, results.TableReference.ProjectId, maxStreamCount, activity, context.CancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
};
IEnumerable<IArrowReader> readers = await ExecuteWithRetriesAsync(getArrowReadersFunc, activity, cancellationContext.CancellationToken).ConfigureAwait(false);
// Note: MultiArrowReader must dispose the cancellationContext.
IArrowArrayStream stream = new MultiArrowReader(this, TranslateSchema(results.Schema), readers, new CancellationContext(cancellationRegistry));
activity?.AddTag(SemanticConventions.Db.Response.ReturnedRows, totalRows);
return new QueryResult(totalRows, stream);
});
}
private async Task<IEnumerable<IArrowReader>> GetArrowReaders(
TokenProtectedReadClientManger clientMgr,
string table,
string projectId,
int maxStreamCount,
Activity? activity,
CancellationToken cancellationToken = default)
{
ReadSession rs = new ReadSession { Table = table, DataFormat = DataFormat.Arrow };
BigQueryReadClient bigQueryReadClient = clientMgr.ReadClient;
ReadSession rrs = await bigQueryReadClient.CreateReadSessionAsync("projects/" + projectId, rs, maxStreamCount);
var readers = rrs.Streams
.Select(s => ReadChunk(bigQueryReadClient, s.Name, activity, this.bigQueryConnection.IsSafeToTrace, cancellationToken))
.Where(chunk => chunk != null)
.Cast<IArrowReader>();
return readers;
}
public override UpdateResult ExecuteUpdate()
{
return ExecuteUpdateInternalAsync().GetAwaiter().GetResult();
}
public override void Cancel()
{
this.TraceActivity(_ =>
{
this.cancellationRegistry.CancelAll();
});
}
public override void Dispose()
{
this.cancellationRegistry.Dispose();
base.Dispose();
}
private async Task<UpdateResult> ExecuteUpdateInternalAsync()
{
return await this.TraceActivityAsync(async activity =>
{
GetQueryResultsOptions getQueryResultsOptions = new GetQueryResultsOptions();
if (Options?.TryGetValue(BigQueryParameters.GetQueryResultsOptionsTimeout, out string? timeoutSeconds) == true &&
int.TryParse(timeoutSeconds, out int seconds) &&
seconds >= 0)
{
getQueryResultsOptions.Timeout = TimeSpan.FromSeconds(seconds);
activity?.AddBigQueryParameterTag(BigQueryParameters.GetQueryResultsOptionsTimeout, seconds);
}
activity?.AddConditionalTag(SemanticConventions.Db.Query.Text, SqlQuery, this.bigQueryConnection.IsSafeToTrace);
using JobCancellationContext context = new(cancellationRegistry);
// Cannot set destination table in jobs with DDL statements, otherwise an error will be prompted
Func<Task<BigQueryResults?>> getQueryResultsAsyncFunc = async () =>
{
return await ExecuteCancellableJobAsync(context, activity, async (context) =>
{
context.Job = await this.Client.CreateQueryJobAsync(SqlQuery, null, null, context.CancellationToken).ConfigureAwait(false);
return await context.Job.GetQueryResultsAsync(getQueryResultsOptions, context.CancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
};
BigQueryResults? result = await ExecuteWithRetriesAsync(getQueryResultsAsyncFunc, activity, context.CancellationToken);
long updatedRows = result?.NumDmlAffectedRows.HasValue == true ? result.NumDmlAffectedRows.Value : -1L;
activity?.AddTag(SemanticConventions.Db.Response.ReturnedRows, updatedRows);
return new UpdateResult(updatedRows);
});
}
private Schema TranslateSchema(TableSchema schema)
{
return new Schema(schema.Fields.Select(TranslateField), null);
}
private Field TranslateField(TableFieldSchema field)
{
List<KeyValuePair<string, string>> metadata = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("BIGQUERY_TYPE", field.Type),
new KeyValuePair<string, string>("BIGQUERY_MODE", field.Mode)
};
return new Field(field.Name, TranslateType(field), field.Mode == "NULLABLE", metadata);
}
private IArrowType TranslateType(TableFieldSchema field)
{
// per https://developers.google.com/resources/api-libraries/documentation/bigquery/v2/java/latest/com/google/api/services/bigquery/model/TableFieldSchema.html#getType--
switch (field.Type)
{
case "INTEGER" or "INT64":
return GetType(field, Int64Type.Default);
case "FLOAT" or "FLOAT64":
return GetType(field, DoubleType.Default);
case "BOOL" or "BOOLEAN":
return GetType(field, BooleanType.Default);
case "STRING":
return GetType(field, StringType.Default);
case "BYTES":
return GetType(field, BinaryType.Default);
case "DATETIME":
return GetType(field, TimestampType.Default);
case "TIMESTAMP":
return GetType(field, TimestampType.Default);
case "TIME":
return GetType(field, Time64Type.Microsecond);
case "DATE":
return GetType(field, Date32Type.Default);
case "RECORD" or "STRUCT":
return GetType(field, BuildStructType(field));
// treat these values as strings
case "GEOGRAPHY" or "JSON":
return GetType(field, StringType.Default);
// get schema cannot get precision and scale for NUMERIC or BIGNUMERIC types
// instead, the max values are returned from BigQuery
// see 'precision' on https://cloud.google.com/bigquery/docs/reference/rest/v2/tables
// and discussion in https://github.com/apache/arrow-adbc/pull/1192#discussion_r1365987279
case "NUMERIC" or "DECIMAL":
return GetType(field, new Decimal128Type(38, 9));
case "BIGNUMERIC" or "BIGDECIMAL":
if (Options != null)
return bool.Parse(Options[BigQueryParameters.LargeDecimalsAsString]) ? GetType(field, StringType.Default) : GetType(field, new Decimal256Type(76, 38));
else
return GetType(field, StringType.Default);
default: throw new InvalidOperationException($"{field.Type} cannot be translated");
}
}
private StructType BuildStructType(TableFieldSchema field)
{
List<Field> arrowFields = new List<Field>();
foreach (TableFieldSchema subfield in field.Fields)
{
Field arrowField = TranslateField(subfield);
arrowFields.Add(arrowField);
}
return new StructType(arrowFields.AsReadOnly());
}
private IArrowType GetType(TableFieldSchema field, IArrowType type)
{
if (field.Mode == "REPEATED")
return new ListType(type);
return type;
}
private static IArrowReader? ReadChunk(BigQueryReadClient client, string streamName, Activity? activity, bool isSafeToTrace, CancellationToken cancellationToken = default)
{
// Ideally we wouldn't need to indirect through a stream, but the necessary APIs in Arrow
// are internal. (TODO: consider changing Arrow).
activity?.AddConditionalBigQueryTag("read_stream", streamName, isSafeToTrace);
BigQueryReadClient.ReadRowsStream readRowsStream = client.ReadRows(new ReadRowsRequest { ReadStream = streamName });
IAsyncEnumerator<ReadRowsResponse> enumerator = readRowsStream.GetResponseStream().GetAsyncEnumerator(cancellationToken);
ReadRowsStream stream = new ReadRowsStream(enumerator);
activity?.AddBigQueryTag("read_stream.has_rows", stream.HasRows);
return stream.HasRows ? stream : null;
}
private QueryOptions ValidateOptions(Activity? activity)
{
QueryOptions options = new QueryOptions();
if (Client.ProjectId == BigQueryConstants.DetectProjectId)
{
activity?.AddBigQueryTag("client_project_id", BigQueryConstants.DetectProjectId);
// An error occurs when calling CreateQueryJob without the ID set,
// so use the first one that is found. This does not prevent from calling
// to other 'project IDs' (catalogs) with a query.
Func<Task<PagedEnumerable<ProjectList, CloudProject>?>> func = () => Task.Run(() =>
{
return Client?.ListProjects();
});
PagedEnumerable<ProjectList, CloudProject>? projects = ExecuteWithRetriesAsync<PagedEnumerable<ProjectList, CloudProject>?>(func, activity).GetAwaiter().GetResult();
if (projects != null)
{
string? firstProjectId = projects.Select(x => x.ProjectId).FirstOrDefault();
if (firstProjectId != null)
{
options.ProjectId = firstProjectId;
activity?.AddBigQueryTag("detected_client_project_id", firstProjectId);
// need to reopen the Client with the projectId specified
this.bigQueryConnection.Open(firstProjectId);
}
}
}
if (Options == null || Options.Count == 0)
return options;
string largeResultDatasetId = BigQueryConstants.DefaultLargeDatasetId;
foreach (KeyValuePair<string, string> keyValuePair in Options)
{
switch (keyValuePair.Key)
{
case BigQueryParameters.AllowLargeResults:
options.AllowLargeResults = true ? keyValuePair.Value.Equals("true", StringComparison.OrdinalIgnoreCase) : false;
activity?.AddBigQueryParameterTag(BigQueryParameters.AllowLargeResults, options.AllowLargeResults);
break;
case BigQueryParameters.LargeResultsDataset:
largeResultDatasetId = keyValuePair.Value;
activity?.AddBigQueryParameterTag(BigQueryParameters.LargeResultsDataset, largeResultDatasetId);
break;
case BigQueryParameters.LargeResultsDestinationTable:
string destinationTable = keyValuePair.Value;
if (!destinationTable.Contains("."))
throw new InvalidOperationException($"{BigQueryParameters.LargeResultsDestinationTable} is invalid");
string projectId = string.Empty;
string datasetId = string.Empty;
string tableId = string.Empty;
string[] segments = destinationTable.Split('.');
if (segments.Length != 3)
throw new InvalidOperationException($"{BigQueryParameters.LargeResultsDestinationTable} cannot be parsed");
projectId = segments[0];
datasetId = segments[1];
tableId = segments[2];
if (string.IsNullOrEmpty(projectId.Trim()) || string.IsNullOrEmpty(datasetId.Trim()) || string.IsNullOrEmpty(tableId.Trim()))
throw new InvalidOperationException($"{BigQueryParameters.LargeResultsDestinationTable} contains invalid values");
options.DestinationTable = new TableReference()
{
ProjectId = projectId,
DatasetId = datasetId,
TableId = tableId
};
activity?.AddBigQueryParameterTag(BigQueryParameters.LargeResultsDestinationTable, destinationTable);
break;
case BigQueryParameters.UseLegacySQL:
options.UseLegacySql = true ? keyValuePair.Value.Equals("true", StringComparison.OrdinalIgnoreCase) : false;
activity?.AddBigQueryParameterTag(BigQueryParameters.UseLegacySQL, options.UseLegacySql);
break;
}
}
if (options.AllowLargeResults == true && options.DestinationTable == null)
{
options.DestinationTable = TryGetLargeDestinationTableReference(largeResultDatasetId, activity);
}
return options;
}
/// <summary>
/// Attempts to retrieve or create the specified dataset.
/// </summary>
/// <param name="datasetId">The name of the dataset.</param>
/// <returns>A <see cref="TableReference"/> to a randomly generated table name in the specified dataset.</returns>
private TableReference TryGetLargeDestinationTableReference(string datasetId, Activity? activity)
{
BigQueryDataset? dataset = null;
try
{
activity?.AddBigQueryTag("large_results.dataset.try_find", datasetId);
dataset = this.Client.GetDataset(datasetId);
activity?.AddBigQueryTag("large_results.dataset.found", datasetId);
}
catch (GoogleApiException gaEx)
{
if (gaEx.HttpStatusCode != System.Net.HttpStatusCode.NotFound)
{
activity?.AddException(gaEx);
throw new AdbcException($"Failure trying to retrieve dataset {datasetId}", gaEx);
}
}
if (dataset == null)
{
try
{
activity?.AddBigQueryTag("large_results.dataset.try_create", datasetId);
activity?.AddBigQueryTag("large_results.dataset.try_create_region", this.Client.DefaultLocation);
DatasetReference reference = this.Client.GetDatasetReference(datasetId);
// The location is not set here because it will use the DefaultLocation from the client.
// Similar to the DefaultLocation for the client, if the caller attempts to use a public
// dataset from a multi-region but set the destination to a specific location,
// a similar permission error is thrown.
BigQueryDataset bigQueryDataset = new BigQueryDataset(this.Client, new Dataset()
{
DatasetReference = reference,
DefaultTableExpirationMs = (long)TimeSpan.FromDays(1).TotalMilliseconds
});
dataset = this.Client.CreateDataset(datasetId, bigQueryDataset.Resource);
activity?.AddBigQueryTag("large_results.dataset.created", datasetId);
}
catch (Exception ex)
{
activity?.AddException(ex);
throw new AdbcException($"Could not create dataset {datasetId} in {this.Client.DefaultLocation}", ex);
}
}
if (dataset == null)
{
throw new AdbcException($"Could not find dataset {datasetId}", AdbcStatusCode.NotFound);
}
else
{
TableReference reference = new TableReference()
{
ProjectId = this.Client.ProjectId,
DatasetId = datasetId,
TableId = "lg_" + Guid.NewGuid().ToString().Replace("-", "")
};
activity?.AddBigQueryTag("large_results.table_reference", reference.ToString());
return reference;
}
}
public bool TokenRequiresUpdate(Exception ex) => BigQueryUtils.TokenRequiresUpdate(ex);
private async Task<T> ExecuteWithRetriesAsync<T>(Func<Task<T>> action, Activity? activity, CancellationToken cancellationToken = default) =>
await RetryManager.ExecuteWithRetriesAsync<T>(this, action, activity, MaxRetryAttempts, RetryDelayMs, cancellationToken);
private async Task<T> ExecuteCancellableJobAsync<T>(
JobCancellationContext context,
Activity? activity,
Func<JobCancellationContext, Task<T>> func)
{
try
{
return await func(context).ConfigureAwait(false);
}
catch (Exception ex)
when (context.CancellationToken.IsCancellationRequested &&
BigQueryUtils.ContainsException(ex, out OperationCanceledException? cancelledEx))
{
// Note: OperationCanceledException could be thrown from the call,
// but we only want to handle when the cancellation was requested from the context.
activity?.AddException(cancelledEx!);
try
{
if (context.Job != null)
{
activity?.AddBigQueryTag("job.cancel", context.Job.Reference.JobId);
await context.Job.CancelAsync().ConfigureAwait(false);
}
}
catch (Exception e)
{
activity?.AddException(e);
}
throw;
}
finally
{
// Job is no longer in context after completion or cancellation
context.Job = null;
}
}
private class CancellationContext : IDisposable
{
private readonly CancellationRegistry cancellationRegistry;
private readonly CancellationTokenSource cancellationTokenSource;
private bool disposed;
public CancellationContext(CancellationRegistry cancellationRegistry)
{
cancellationTokenSource = new CancellationTokenSource();
this.cancellationRegistry = cancellationRegistry;
this.cancellationRegistry.Register(this);
}
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public void Cancel()
{
cancellationTokenSource.Cancel();
}
public virtual void Dispose()
{
if (!disposed)
{
cancellationRegistry.Unregister(this);
cancellationTokenSource.Dispose();
disposed = true;
}
}
}
private class JobCancellationContext : CancellationContext
{
public JobCancellationContext(CancellationRegistry cancellationRegistry, BigQueryJob? job = default)
: base(cancellationRegistry)
{
Job = job;
}
public BigQueryJob? Job { get; set; }
}
private sealed class CancellationRegistry : IDisposable
{
private readonly ConcurrentDictionary<CancellationContext, byte> contexts = new();
private bool disposed;
public CancellationContext Register(CancellationContext context)
{
if (disposed) throw new ObjectDisposedException(nameof(CancellationRegistry));
contexts.TryAdd(context, 0);
return context;
}
public bool Unregister(CancellationContext context)
{
if (disposed) return false;
return contexts.TryRemove(context, out _);
}
public void CancelAll()
{
if (disposed) throw new ObjectDisposedException(nameof(CancellationRegistry));
foreach (CancellationContext context in contexts.Keys)
{
context.Cancel();
}
}
public void Dispose()
{
if (!disposed)
{
contexts.Clear();
disposed = true;
}
}
}
private class MultiArrowReader : TracingReader
{
private static readonly string s_assemblyName = BigQueryUtils.GetAssemblyName(typeof(BigQueryStatement));
private static readonly string s_assemblyVersion = BigQueryUtils.GetAssemblyVersion(typeof(BigQueryStatement));
readonly Schema schema;
readonly CancellationContext cancellationContext;
IEnumerator<IArrowReader>? readers;
IArrowReader? reader;
bool disposed;
public MultiArrowReader(BigQueryStatement statement, Schema schema, IEnumerable<IArrowReader> readers, CancellationContext cancellationContext) : base(statement)
{
this.schema = schema;
this.readers = readers.GetEnumerator();
this.cancellationContext = cancellationContext;
}
public override Schema Schema { get { return this.schema; } }
public override string AssemblyVersion => s_assemblyVersion;
public override string AssemblyName => s_assemblyName;
public override async ValueTask<RecordBatch?> ReadNextRecordBatchAsync(CancellationToken cancellationToken = default)
{
return await this.TraceActivityAsync(async activity =>
{
if (this.readers == null)
{
return null;
}
using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this.cancellationContext.CancellationToken);
while (true)
{
linkedCts.Token.ThrowIfCancellationRequested();
if (this.reader == null)
{
if (!this.readers.MoveNext())
{
this.readers.Dispose();
this.readers = null;
return null;
}
this.reader = this.readers.Current;
}
RecordBatch result = await this.reader.ReadNextRecordBatchAsync(linkedCts.Token).ConfigureAwait(false);
if (result != null)
{
return result;
}
this.reader = null;
}
});
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (!this.disposed)
{
if (this.readers != null)
{
this.readers.Dispose();
this.readers = null;
}
this.cancellationContext.Dispose();
this.disposed = true;
}
}
base.Dispose(disposing);
}
}
sealed class ReadRowsStream : IArrowArrayStream
{
readonly Schema? schema;
readonly IAsyncEnumerator<ReadRowsResponse> response;
bool first;
bool disposed;
public ReadRowsStream(IAsyncEnumerator<ReadRowsResponse> response)
{
try
{
if (response.MoveNextAsync().Result && response.Current != null)
{
this.schema = ArrowSerializationHelpers.DeserializeSchema(response.Current.ArrowSchema.SerializedSchema.Memory);
}
}
catch (InvalidOperationException)
{
}
this.response = response;
this.first = true;
}
public Schema Schema => this.schema ?? throw new InvalidOperationException("Stream has no rows");
public bool HasRows => this.schema != null;
public async ValueTask<RecordBatch?> ReadNextRecordBatchAsync(CancellationToken cancellationToken)
{
if (this.first)
{
this.first = false;
}
else if (this.disposed || !await this.response.MoveNextAsync())
{
return null;
}
return ArrowSerializationHelpers.DeserializeRecordBatch(this.schema, this.response.Current.ArrowRecordBatch.SerializedRecordBatch.Memory);
}
public void Dispose()
{
if (!this.disposed)
{
this.response.DisposeAsync().GetAwaiter().GetResult();
this.disposed = true;
}
}
}
}
}