7070import java .util .Comparator ;
7171import java .util .HashSet ;
7272import java .util .List ;
73+ import java .util .Locale ;
7374import java .util .Set ;
7475import java .util .concurrent .BlockingQueue ;
7576import 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 ) {
0 commit comments