Skip to content

Commit 148edc3

Browse files
committed
initial
1 parent 5196676 commit 148edc3

12 files changed

Lines changed: 825 additions & 18 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
## I/Os
2929
3030
* Support for X source added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
31+
* (Java) BigQueryIO now supports reading BigQuery Lakehouse runtime catalog (BigLake metastore) Iceberg tables with the Storage Read API, using 4-part `project.catalog.namespace.table` identifiers (or a `TableReference` with a composite `catalog.namespace` dataset id). Previously such references were silently mis-parsed, and tables without storage statistics failed with a `NullPointerException`.
3132
3233
## New Features / Improvements
3334

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryHelpers.java

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import java.util.Map;
4646
import java.util.UUID;
4747
import java.util.regex.Matcher;
48+
import java.util.regex.Pattern;
4849
import java.util.stream.Collectors;
4950
import org.apache.beam.sdk.extensions.gcp.util.BackOffAdapter;
5051
import org.apache.beam.sdk.io.FileSystems;
@@ -79,6 +80,9 @@ public class BigQueryHelpers {
7980

8081
private static final Logger LOG = LoggerFactory.getLogger(BigQueryHelpers.class);
8182

83+
/** Matches valid Project ID patterns. */
84+
private static final Pattern PROJECT_NAME_SEGMENT_PATTERN = Pattern.compile("[-a-z0-9]*[a-z0-9]");
85+
8286
// Given a potential failure and a current job-id, return the next job-id to be used on retry.
8387
// Algorithm is as follows (given input of job_id_prefix-N)
8488
// If BigQuery has no status for job_id_prefix-n, we should retry with the same id.
@@ -458,6 +462,13 @@ static <K, V> List<V> getOrCreateMapListValue(Map<K, List<V>> map, K key) {
458462
* Parse a table specification in the form {@code "[project_id]:[dataset_id].[table_id]"} or
459463
* {@code "[project_id].[dataset_id].[table_id]"} or {@code "[dataset_id].[table_id]"}.
460464
*
465+
* <p>Lakehouse runtime catalog (BigLake metastore) tables are referenced with four parts, {@code
466+
* "[project_id].[catalog_id].[namespace_id].[table_id]"} (or {@code
467+
* "[project_id]:[catalog_id].[namespace_id].[table_id]"}); these parse to a composite {@code
468+
* "[catalog_id].[namespace_id]"} dataset id, which is the form the BigQuery APIs accept for such
469+
* tables. More generally, when a specification contains more than three segments, everything
470+
* between the project id and the final (table) segment becomes the dataset id.
471+
*
461472
* <p>If the project id is omitted, the default project id is used.
462473
*/
463474
@SuppressWarnings({
@@ -471,14 +482,75 @@ public static TableReference parseTableSpec(String tableSpec) {
471482
"Table specification [%s] is not in one of the expected formats ("
472483
+ " [project_id]:[dataset_id].[table_id],"
473484
+ " [project_id].[dataset_id].[table_id],"
474-
+ " [dataset_id].[table_id])",
485+
+ " [dataset_id].[table_id],"
486+
+ " [project_id]:[catalog_id].[namespace_id].[table_id],"
487+
+ " [project_id].[catalog_id].[namespace_id].[table_id])",
475488
tableSpec));
476489
}
477490

478-
TableReference ref = new TableReference();
479-
ref.setProjectId(match.group("PROJECT"));
491+
// Table ids cannot contain '.', so the table is always the segment after
492+
// the last dot.
493+
int lastDot = tableSpec.lastIndexOf('.');
494+
String table = tableSpec.substring(lastDot + 1);
495+
String prefix = tableSpec.substring(0, lastDot);
496+
497+
String project = null;
498+
String dataset;
499+
long colonCount = prefix.chars().filter(c -> c == ':').count();
500+
if (colonCount == 0) {
501+
// No colon means the purely dotted form ("p.d.t", "d.t", "p.catalog.ns.t"): the
502+
// leading segment is the project id when it is a plausible project id.
503+
// (Dataset ids may contain characters such as '_' that project ids may
504+
// not, in which case the whole prefix is the dataset id.)
505+
// The firstDot < length-1 guard keeps degenerate trailing-dot specs
506+
// ("pp..t", accepted by the character-set gate with dataset "pp.")
507+
// instead of producing an empty dataset id.
508+
int firstDot = prefix.indexOf('.');
509+
if (firstDot >= 0
510+
&& firstDot < prefix.length() - 1
511+
&& BigQueryIO.PROJECT_ID_PATTERN.matcher(prefix.substring(0, firstDot)).matches()) {
512+
project = prefix.substring(0, firstDot);
513+
dataset = prefix.substring(firstDot + 1);
514+
} else {
515+
dataset = prefix;
516+
}
517+
} else if (colonCount == 1) {
518+
// One colon ("p:d.t", "p:catalog.ns.t", "example.com:proj.ds.t"). If the
519+
// project part is dotted, it is a legacy domain-scoped id written with
520+
// a '.' separator after the project: the first dataset segment completes
521+
// the project id, and any remaining middle segments bind as a (possibly
522+
// composite) dataset. (Domain-scoped project names cannot contain dots)
523+
int colon = prefix.indexOf(':');
524+
project = prefix.substring(0, colon);
525+
dataset = prefix.substring(colon + 1);
526+
int firstDot = dataset.indexOf('.');
527+
// Absorb the first dataset segment into a dotted (domain-scoped) project
528+
// only when the split leaves a non-empty dataset.
529+
if (firstDot >= 0
530+
&& firstDot < dataset.length() - 1
531+
&& project.indexOf('.') >= 0
532+
&& PROJECT_NAME_SEGMENT_PATTERN.matcher(dataset.substring(0, firstDot)).matches()) {
533+
project = project + ":" + dataset.substring(0, firstDot);
534+
dataset = dataset.substring(firstDot + 1);
535+
}
536+
} else {
537+
// Two colons - the last colon is an explicit project terminator. This is
538+
// the canonical spelling for a domain-scoped project, whose id itself
539+
// contains a colon ("example.com:proj:ds.t"), including with a composite
540+
// Lakehouse catalog dataset ("example.com:proj:catalog.ns.t"). Both
541+
// domain-scoped spellings keep toTableSpec/parseTableSpec a round trip
542+
// for composite dataset ids. (More than two colons cannot form a valid
543+
// reference - project ids contain at most one colon, but such specs pass
544+
// the character-set gate, so they bind here too and the impossible
545+
// project id is rejected by the service.)
546+
int lastColon = prefix.lastIndexOf(':');
547+
project = prefix.substring(0, lastColon);
548+
dataset = prefix.substring(lastColon + 1);
549+
}
480550

481-
return ref.setDatasetId(match.group("DATASET")).setTableId(match.group("TABLE"));
551+
TableReference ref = new TableReference();
552+
ref.setProjectId(project);
553+
return ref.setDatasetId(dataset).setTableId(table);
482554
}
483555

484556
@SuppressWarnings({

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryIO.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,13 @@ public class BigQueryIO {
589589
*/
590590
private static final String PROJECT_ID_REGEXP = "[a-z][-a-z0-9:.]{0,61}[a-z0-9]";
591591

592+
/**
593+
* Matches a whole string against {@link #PROJECT_ID_REGEXP}. Used by {@link
594+
* BigQueryHelpers#parseTableSpec} to decide whether the leading segment of a dotted table
595+
* specification is a project id.
596+
*/
597+
static final Pattern PROJECT_ID_PATTERN = Pattern.compile(PROJECT_ID_REGEXP);
598+
592599
/** Regular expression that matches Dataset IDs. */
593600
private static final String DATASET_REGEXP = "[-\\w.]{1,1024}";
594601

@@ -604,6 +611,12 @@ public class BigQueryIO {
604611
/**
605612
* Matches table specifications in the form {@code "[project_id]:[dataset_id].[table_id]"}, {@code
606613
* "[project_id].[dataset_id].[table_id]"}, or {@code "[dataset_id].[table_id]"}.
614+
*
615+
* <p>This pattern is used for syntactic validation only; the assignment of the matched string's
616+
* segments to the project/dataset/table fields is done by {@link BigQueryHelpers#parseTableSpec},
617+
* which additionally understands 4-part Lakehouse runtime catalog (BigLake metastore) references
618+
* {@code "[project_id].[catalog_id].[namespace_id].[table_id]"}, mapping them to a composite
619+
* {@code "[catalog_id].[namespace_id]"} dataset id.
607620
*/
608621
private static final String DATASET_TABLE_REGEXP =
609622
String.format(

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryStorageSourceBase.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,12 @@ public List<BigQueryStorageStreamSource<T>> split(
152152
int streamCount = 0;
153153
if (!bqOptions.getEnableStorageReadApiV2()) {
154154
if (desiredBundleSizeBytes > 0) {
155-
long tableSizeBytes = (targetTable != null) ? targetTable.getNumBytes() : 0;
155+
// numBytes is null for tables that don't report storage statistics, e.g. Lakehouse
156+
// runtime catalog (BigLake metastore) tables.
157+
long tableSizeBytes =
158+
(targetTable != null && targetTable.getNumBytes() != null)
159+
? targetTable.getNumBytes()
160+
: 0;
156161
streamCount = (int) Math.min(tableSizeBytes / desiredBundleSizeBytes, MAX_SPLIT_COUNT);
157162
}
158163

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryStorageTableSource.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,13 @@ public void populateDisplayData(DisplayData.Builder builder) {
163163
@Override
164164
public long getEstimatedSizeBytes(PipelineOptions options) throws Exception {
165165
Table table = getTargetTable(options.as(BigQueryOptions.class));
166-
if (table != null) {
166+
if (table != null && table.getNumBytes() != null) {
167167
return table.getNumBytes();
168168
}
169169
// If the table does not exist, then it will be null.
170170
// Avoid the NullPointerException here, allow a more meaningful table "not_found"
171171
// error to be shown to the user, upon table read.
172+
// Lakehouse runtime catalog (BigLake metastore) tables exist but report no numBytes.
172173
return 0;
173174
}
174175

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/bigquery/BigQueryTableSource.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ public synchronized long getEstimatedSizeBytes(PipelineOptions options) throws E
8282
}
8383

8484
Long numBytes = table.getNumBytes();
85+
if (numBytes == null) {
86+
// Tables that don't report storage statistics, e.g. Lakehouse runtime catalog
87+
// (BigLake metastore) tables.
88+
numBytes = 0L;
89+
}
8590
if (table.getStreamingBuffer() != null
8691
&& table.getStreamingBuffer().getEstimatedBytes() != null) {
8792
numBytes += table.getStreamingBuffer().getEstimatedBytes().longValue();

0 commit comments

Comments
 (0)