Skip to content

Commit 4df3ca7

Browse files
feat(bigquery-jdbc): add Graph metadata support in getTables and getTableTypes methods (#14868)
b/570033146 b/570032972 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 4d6641c commit 4df3ca7

5 files changed

Lines changed: 399 additions & 40 deletions

File tree

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ public class BigQueryConnection extends BigQueryNoOpsConnection {
164164
BigQueryJdbcUrlUtility.OAUTH_SA_IMPERSONATION_TOKEN_LIFETIME_PROPERTY_NAME,
165165
BigQueryJdbcUrlUtility.OAUTH_SA_EMAIL_PROPERTY_NAME,
166166
BigQueryJdbcUrlUtility.OAUTH_PVT_KEY_PATH_PROPERTY_NAME,
167-
BigQueryJdbcUrlUtility.OAUTH_CLIENT_ID_PROPERTY_NAME)
167+
BigQueryJdbcUrlUtility.OAUTH_CLIENT_ID_PROPERTY_NAME,
168+
BigQueryJdbcUrlUtility.ENABLE_DIAGNOSTIC_TELEMETRY_PROPERTY_NAME,
169+
BigQueryJdbcUrlUtility.ENABLE_GRAPH_METADATA_PROPERTY_NAME)
168170
.build();
169171
private HeaderProvider headerProvider;
170172
BigQueryReadClient bigQueryReadClient = null;

‎java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java‎

Lines changed: 184 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import java.util.Comparator;
7171
import java.util.HashSet;
7272
import java.util.List;
73+
import java.util.Locale;
7374
import java.util.Set;
7475
import java.util.concurrent.BlockingQueue;
7576
import java.util.concurrent.Callable;
@@ -103,6 +104,8 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData {
103104
private static final int DEFAULT_QUEUE_CAPACITY = 5000;
104105
private static final String GET_EXPORTED_KEYS_SQL = "DatabaseMetaData_GetExportedKeys.sql";
105106
private static String exportedKeysSqlContent;
107+
private static final String GET_GRAPH_TABLES_SQL = "DatabaseMetaData_GetGraphTables.sql";
108+
private static String graphTablesSqlContent;
106109
// Declared package-private for testing.
107110
static final String GOOGLE_SQL_QUOTED_IDENTIFIER = "`";
108111
// Does not include SQL:2003 Keywords as per JDBC spec.
@@ -138,6 +141,24 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData {
138141
static final String GOOGLE_SQL_CATALOG_SEPARATOR = ".";
139142
static final int GOOGLE_SQL_MAX_COL_NAME_LEN = 300;
140143
static final int GOOGLE_SQL_MAX_COLS_PER_TABLE = 10000;
144+
private static final String[] DEFAULT_TABLE_TYPES = {
145+
"EXTERNAL", "MATERIALIZED VIEW", "SNAPSHOT", "TABLE", "VIEW"
146+
};
147+
private static final String[] TABLE_TYPES_WITH_GRAPH = {
148+
"EXTERNAL", "GRAPH TABLE", "MATERIALIZED VIEW", "SNAPSHOT", "TABLE", "VIEW"
149+
};
150+
static final String GRAPH_TABLE_TYPE = "GRAPH TABLE";
151+
private static final String DEFAULT_GRAPH_REGION = "us";
152+
// Google Cloud project IDs: letters, digits, hyphens, and optional domain prefix (e.g.
153+
// domain.com:proj-1)
154+
private static final Pattern VALID_CATALOG_PATTERN =
155+
Pattern.compile("^[A-Za-z0-9][A-Za-z0-9:._-]*$");
156+
// BigQuery dataset IDs: letters, digits, and underscores only
157+
private static final Pattern VALID_SCHEMA_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$");
158+
// BigQuery region qualifier for regional INFORMATION_SCHEMA queries (e.g. region-us,
159+
// region-us-central1)
160+
private static final Pattern VALID_REGION_SCOPE_PATTERN =
161+
Pattern.compile("^region-[A-Za-z0-9-]+$");
141162

142163
String URL;
143164
BigQueryConnection connection;
@@ -1308,19 +1329,37 @@ private ResultSet getTablesImpl(
13081329
() -> {
13091330
final List<FieldValueList> collectedResults =
13101331
Collections.synchronizedList(new ArrayList<>());
1311-
processTargetTablesConcurrently(
1312-
targetDatasets,
1332+
boolean onlyGraphTablesRequested =
1333+
requestedTypes != null
1334+
&& requestedTypes.size() == 1
1335+
&& requestedTypes.contains(GRAPH_TABLE_TYPE);
1336+
boolean isExactGraphTableName =
1337+
tableNamePattern != null
1338+
&& !tableNamePattern.contains("%")
1339+
&& tableNamePattern.contains(".");
1340+
if (!isExactGraphTableName && !onlyGraphTablesRequested) {
1341+
processTargetTablesConcurrently(
1342+
targetDatasets,
1343+
tableNamePattern,
1344+
true,
1345+
false,
1346+
false,
1347+
collectedResults,
1348+
resultSchemaFields,
1349+
(bqTable, results, fields) -> {
1350+
processTableInfo(bqTable, requestedTypes, results, fields);
1351+
},
1352+
TableOption.fields(
1353+
TableField.TABLE_REFERENCE, TableField.TYPE, TableField.DESCRIPTION));
1354+
}
1355+
1356+
appendGraphTableRows(
1357+
effectiveCatalog,
1358+
effectiveSchemaPattern,
13131359
tableNamePattern,
1314-
true,
1315-
false,
1316-
false,
1360+
requestedTypes,
13171361
collectedResults,
1318-
resultSchemaFields,
1319-
(bqTable, results, fields) -> {
1320-
processTableInfo(bqTable, requestedTypes, results, fields);
1321-
},
1322-
TableOption.fields(
1323-
TableField.TABLE_REFERENCE, TableField.TYPE, TableField.DESCRIPTION));
1362+
resultSchemaFields);
13241363

13251364
Comparator<FieldValueList> comparator = defineGetTablesComparator(resultSchemaFields);
13261365
sortResults(collectedResults, comparator, "getTables", LOG);
@@ -1441,6 +1480,123 @@ Comparator<FieldValueList> defineGetTablesComparator(FieldList resultSchemaField
14411480
Comparator.nullsFirst(String::compareTo));
14421481
}
14431482

1483+
static void validateCatalogIdentifier(String catalog) throws SQLException {
1484+
if (catalog == null || !VALID_CATALOG_PATTERN.matcher(catalog).matches()) {
1485+
throw new BigQueryJdbcException(
1486+
String.format("Invalid catalog (project ID) identifier: '%s'", catalog));
1487+
}
1488+
}
1489+
1490+
static void validateSchemaIdentifier(String schema) throws SQLException {
1491+
if (schema == null || !VALID_SCHEMA_PATTERN.matcher(schema).matches()) {
1492+
throw new BigQueryJdbcException(
1493+
String.format("Invalid schema (dataset ID) identifier: '%s'", schema));
1494+
}
1495+
}
1496+
1497+
static void validateRegionScopeIdentifier(String regionScope) throws SQLException {
1498+
if (regionScope == null || !VALID_REGION_SCOPE_PATTERN.matcher(regionScope).matches()) {
1499+
throw new BigQueryJdbcException(
1500+
String.format("Invalid region scope identifier: '%s'", regionScope));
1501+
}
1502+
}
1503+
1504+
boolean shouldSkipGraphTables(String tableNamePattern, Set<String> requestedTypes) {
1505+
if (this.connection == null || !this.connection.isEnableGraphMetadata()) {
1506+
return true;
1507+
}
1508+
if (requestedTypes != null && !requestedTypes.contains(GRAPH_TABLE_TYPE)) {
1509+
return true;
1510+
}
1511+
if (tableNamePattern != null
1512+
&& !tableNamePattern.contains("%")
1513+
// If table name has a dot it is expected to be a graph table name.
1514+
&& !tableNamePattern.contains(".")) {
1515+
return true;
1516+
}
1517+
return false;
1518+
}
1519+
1520+
String resolveGraphRegion() throws SQLException {
1521+
String location = this.connection != null ? this.connection.getLocation() : null;
1522+
if (location == null || location.trim().isEmpty()) {
1523+
location = DEFAULT_GRAPH_REGION;
1524+
LOG.info(String.format("Deafulting to Graph Location %s", DEFAULT_GRAPH_REGION));
1525+
}
1526+
String normalized = location.trim().toLowerCase(Locale.US);
1527+
return normalized.startsWith("region-") ? normalized : "region-" + normalized;
1528+
}
1529+
1530+
void appendGraphTableRows(
1531+
String effectiveCatalog,
1532+
String effectiveSchemaPattern,
1533+
String tableNamePattern,
1534+
Set<String> requestedTypes,
1535+
List<FieldValueList> collectedResults,
1536+
FieldList resultSchemaResults)
1537+
throws SQLException {
1538+
if (shouldSkipGraphTables(tableNamePattern, requestedTypes)) {
1539+
return;
1540+
}
1541+
1542+
LOG.info("Appending Graph table rows");
1543+
1544+
List<String> targetCatalogs =
1545+
effectiveCatalog != null
1546+
? Collections.singletonList(effectiveCatalog)
1547+
: getAccessibleCatalogNames();
1548+
boolean isExactDataset =
1549+
effectiveSchemaPattern != null && !effectiveSchemaPattern.contains("%");
1550+
String effectiveTablePattern = (tableNamePattern == null) ? "%" : tableNamePattern;
1551+
String scope = isExactDataset ? effectiveSchemaPattern : resolveGraphRegion();
1552+
if (isExactDataset) {
1553+
validateSchemaIdentifier(scope);
1554+
} else {
1555+
validateRegionScopeIdentifier(scope);
1556+
}
1557+
for (String project : targetCatalogs) {
1558+
validateCatalogIdentifier(project);
1559+
String formattedSql = replaceSqlParameters(getGraphTablesSqlContent(), project, scope);
1560+
1561+
try (PreparedStatement statement = this.connection.prepareStatement(formattedSql)) {
1562+
statement.setString(1, effectiveSchemaPattern == null ? "%" : effectiveSchemaPattern);
1563+
statement.setString(2, effectiveTablePattern);
1564+
1565+
try (ResultSet rs = statement.executeQuery()) {
1566+
while (rs.next()) {
1567+
String tableCat = rs.getString("TABLE_CAT");
1568+
String tableSchem = rs.getString("TABLE_SCHEM");
1569+
String tableName = rs.getString("TABLE_NAME");
1570+
String sourceTable = rs.getString("SOURCE_TABLE");
1571+
String remarks = sourceTable != null ? "Source: " + sourceTable : null;
1572+
1573+
List<FieldValue> values = new ArrayList<>(resultSchemaResults.size());
1574+
values.add(createStringFieldValue(tableCat)); // 1. TABLE_CAT
1575+
values.add(createStringFieldValue(tableSchem)); // 2. TABLE_SCHEM
1576+
values.add(createStringFieldValue(tableName)); // 3. TABLE_NAME
1577+
values.add(createStringFieldValue(GRAPH_TABLE_TYPE)); // 4. TABLE_TYPE
1578+
values.add(createStringFieldValue(remarks)); // 5. REMARKS
1579+
values.add(createNullFieldValue()); // 6. TYPE_CAT (always null)
1580+
values.add(createNullFieldValue()); // 7. TYPE_SCHEM (always null)
1581+
values.add(createNullFieldValue()); // 8. TYPE_NAME (always null)
1582+
values.add(createNullFieldValue()); // 9. SELF_REFERENCING_COL_NAME (always null)
1583+
values.add(createNullFieldValue()); // 10. REF_GENERATION (always null)
1584+
collectedResults.add(FieldValueList.of(values, resultSchemaResults));
1585+
}
1586+
}
1587+
} catch (SQLException e) {
1588+
if (e.getMessage() != null && e.getMessage().contains("Not found")) {
1589+
LOG.info(
1590+
"Dataset or project not found while querying PROPERTY_GRAPHS in project '%s'."
1591+
+ " Skipping.",
1592+
project);
1593+
} else {
1594+
throw e;
1595+
}
1596+
}
1597+
}
1598+
}
1599+
14441600
@Override
14451601
public ResultSet getSchemas() throws SQLException {
14461602
LOG.info("getSchemas() called");
@@ -1488,7 +1644,8 @@ public ResultSet getTableTypes() throws SQLException {
14881644
LOG.info("getTableTypes() called");
14891645

14901646
final Schema tableTypesSchema = defineGetTableTypesSchema();
1491-
final List<FieldValueList> tableTypeRows = prepareGetTableTypesRows(tableTypesSchema);
1647+
final List<FieldValueList> tableTypeRows =
1648+
prepareGetTableTypesRows(tableTypesSchema, this.connection.isEnableGraphMetadata());
14921649

14931650
BlockingQueue<BigQueryFieldValueListWrapper> queue =
14941651
fetchAndPopulateQueue(() -> tableTypeRows, tableTypesSchema.getFields());
@@ -1504,9 +1661,13 @@ static Schema defineGetTableTypesSchema() {
15041661
}
15051662

15061663
static List<FieldValueList> prepareGetTableTypesRows(Schema schema) {
1507-
final String[] tableTypes = {"EXTERNAL", "MATERIALIZED VIEW", "SNAPSHOT", "TABLE", "VIEW"};
1508-
List<FieldValueList> rows = new ArrayList<>(tableTypes.length);
1664+
return prepareGetTableTypesRows(schema, false);
1665+
}
1666+
1667+
static List<FieldValueList> prepareGetTableTypesRows(Schema schema, boolean includeGraphTables) {
1668+
final String[] tableTypes = includeGraphTables ? TABLE_TYPES_WITH_GRAPH : DEFAULT_TABLE_TYPES;
15091669
FieldList schemaFields = schema.getFields();
1670+
List<FieldValueList> rows = new ArrayList<>(tableTypes.length);
15101671

15111672
for (String typeName : tableTypes) {
15121673
FieldValue fieldValue = FieldValue.of(FieldValue.Attribute.PRIMITIVE, typeName);
@@ -2195,6 +2356,8 @@ public ResultSet getExportedKeys(String catalog, String schema, String table)
21952356
return BigQueryJsonResultSet.of(resultSchema, -1, queue, null);
21962357
}
21972358

2359+
validateCatalogIdentifier(catalog);
2360+
validateSchemaIdentifier(schema);
21982361
String sql = getExportedKeysSqlContent();
21992362
String formattedSql = replaceSqlParameters(sql, catalog, schema, table);
22002363
PreparedStatement stmt = this.connection.prepareStatement(formattedSql);
@@ -4853,6 +5016,13 @@ private static synchronized String getExportedKeysSqlContent() {
48535016
return exportedKeysSqlContent;
48545017
}
48555018

5019+
static synchronized String getGraphTablesSqlContent() {
5020+
if (graphTablesSqlContent == null) {
5021+
graphTablesSqlContent = readSqlFromFile(GET_GRAPH_TABLES_SQL);
5022+
}
5023+
return graphTablesSqlContent;
5024+
}
5025+
48565026
static String readSqlFromFile(String filename) {
48575027
try (InputStream in = BigQueryDatabaseMetaData.class.getResourceAsStream(filename)) {
48585028
if (in == null) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
SELECT
18+
property_graph_catalog AS TABLE_CAT,
19+
property_graph_schema AS TABLE_SCHEM,
20+
CONCAT(property_graph_name, '.', JSON_VALUE(t, '$.name')) AS TABLE_NAME,
21+
CONCAT(
22+
JSON_VALUE(t, '$.dataSourceTable.projectId'), '.',
23+
JSON_VALUE(t, '$.dataSourceTable.datasetId'), '.',
24+
JSON_VALUE(t, '$.dataSourceTable.tableId')) AS SOURCE_TABLE
25+
FROM `%1$s.%2$s.INFORMATION_SCHEMA.PROPERTY_GRAPHS`,
26+
UNNEST(ARRAY_CONCAT(
27+
IFNULL(JSON_QUERY_ARRAY(property_graph_metadata_json, '$.nodeTables'), []),
28+
IFNULL(JSON_QUERY_ARRAY(property_graph_metadata_json, '$.edgeTables'), []))) AS t
29+
WHERE LOWER(property_graph_schema) LIKE LOWER(?)
30+
AND LOWER(CONCAT(property_graph_name, '.', JSON_VALUE(t, '$.name'))) LIKE LOWER(?)

0 commit comments

Comments
 (0)