From 7cb03ce8c2888d4acefbabb9bc29fdf9daa49c19 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Mon, 2 Nov 2026 13:10:54 -0500 Subject: [PATCH 1/2] feat(bigquery-jdbc): add graph integration in getColumns method --- .../jdbc/BigQueryDatabaseMetaData.java | 246 ++++++++++++++---- .../jdbc/DatabaseMetaData_GetGraphColumns.sql | 46 ++++ .../jdbc/BigQueryDatabaseMetaDataTest.java | 117 +++++++++ .../jdbc/it/ITDatabaseMetadataTest.java | 124 +++++++++ 4 files changed, 487 insertions(+), 46 deletions(-) create mode 100644 java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetGraphColumns.sql diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 54d9991997e8..60378bbcdee8 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -109,6 +109,10 @@ class BigQueryDatabaseMetaData implements DatabaseMetaData { private static String graphTablesRegionSqlContent; private static final String GET_GRAPH_TABLES_DATASET_SQL = "DatabaseMetaData_GetGraphTables_Dataset.sql"; + private static final String GET_GRAPH_COLUMNS_SQL = "DatabaseMetaData_GetGraphColumns.sql"; + private static String graphColumnsSqlContent; + static final String MEASURE_EXPRESSION_KIND = "MEASURE"; + static final String MEASURE_REMARKS_PREFIX = "MEASURE: "; private static String graphTablesDatasetSqlContent; // Declared package-private for testing. static final String GOOGLE_SQL_QUOTED_IDENTIFIER = "`"; @@ -1228,25 +1232,16 @@ ColumnTypeInfo determineTypeInfoFromDataType( String procedureName, String columnName, int ordinalPosition) { - - ColumnTypeInfo defaultStringTypeInfo = - new ColumnTypeInfo(Types.NVARCHAR, "STRING", null, null, null); try { - String typeKind = argumentDataType.getTypeKind(); - if (typeKind != null && !typeKind.isEmpty()) { - if ("ARRAY".equalsIgnoreCase(typeKind)) { - return new ColumnTypeInfo(Types.ARRAY, "ARRAY", null, null, null); - } - StandardSQLTypeName determinedType = StandardSQLTypeName.valueOf(typeKind.toUpperCase()); - return getColumnTypeInfoForSqlType(determinedType); - } + return getColumnTypeInfoForTypeKind( + argumentDataType != null ? argumentDataType.getTypeKind() : null); } catch (Exception e) { LOG.warning( "Proc: %s, Arg: %s (Pos %d) - Caught an unexpected Exception during type" + " determination. Defaulting type to STRING. Error: %s", procedureName, columnName, ordinalPosition, e.getMessage()); + return getColumnTypeInfoForSqlType(null); } - return defaultStringTypeInfo; } Comparator defineGetProcedureColumnsComparator(FieldList resultSchemaFields) { @@ -1484,10 +1479,14 @@ private static String quoteIdentifier(String identifier) throws SQLException { } boolean shouldSkipGraphTables(String tableNamePattern, Set requestedTypes) { - if (this.connection == null || !this.connection.isEnableGraphMetadata()) { + if (requestedTypes != null && !requestedTypes.contains(GRAPH_TABLE_TYPE)) { return true; } - if (requestedTypes != null && !requestedTypes.contains(GRAPH_TABLE_TYPE)) { + return shouldSkipGraphColumns(tableNamePattern); + } + + boolean shouldSkipGraphColumns(String tableNamePattern) { + if (this.connection == null || !this.connection.isEnableGraphMetadata()) { return true; } if (tableNamePattern != null @@ -1705,19 +1704,32 @@ private ResultSet getColumnsImpl( () -> { final List collectedResults = Collections.synchronizedList(new ArrayList<>()); - processTargetTablesConcurrently( - targetDatasets, + boolean isExactGraphTableName = + tableNamePattern != null + && !needsListing(tableNamePattern) + && tableNamePattern.contains("."); + if (!isExactGraphTableName) { + processTargetTablesConcurrently( + targetDatasets, + tableNamePattern, + true, + false, + true, + collectedResults, + resultSchemaFields, + (bqTable, results, fields) -> { + processTableColumns(bqTable, columnNameRegex, results, fields); + }, + TableOption.fields( + TableField.TABLE_REFERENCE, TableField.TYPE, TableField.SCHEMA)); + } + appendGraphColumnRows( + effectiveCatalog, + effectiveSchemaPattern, tableNamePattern, - true, - false, - true, + columnNamePattern, collectedResults, - resultSchemaFields, - (bqTable, results, fields) -> { - processTableColumns(bqTable, columnNameRegex, results, fields); - }, - TableOption.fields( - TableField.TABLE_REFERENCE, TableField.TYPE, TableField.SCHEMA)); + resultSchemaFields); Comparator comparator = defineGetColumnsComparator(resultSchemaFields); @@ -1796,7 +1808,120 @@ private void processTableColumns( } } - private Schema defineGetColumnsSchema() { + void appendGraphColumnRows( + String effectiveCatalog, + String effectiveSchemaPattern, + String tableNamePattern, + String columnNamePattern, + List collectedResults, + FieldList resultSchemaFields) + throws SQLException { + if (shouldSkipGraphColumns(tableNamePattern)) { + return; + } + List targetCatalogs = + effectiveCatalog != null + ? Collections.singletonList(effectiveCatalog) + : getAccessibleCatalogNames(); + boolean isExactDataset = + effectiveSchemaPattern != null && !needsListing(effectiveSchemaPattern); + String effectiveSchemaLike = (effectiveSchemaPattern == null) ? "%" : effectiveSchemaPattern; + String effectiveTableLike = (tableNamePattern == null) ? "%" : tableNamePattern; + String effectiveColumnLike = (columnNamePattern == null) ? "%" : columnNamePattern; + for (String project : targetCatalogs) { + String scope = isExactDataset ? effectiveSchemaPattern : resolveGraphRegion(); + String formattedSql = + replaceSqlParameters( + getGraphColumnsSqlContent(), quoteIdentifier(project), quoteIdentifier(scope)); + try (PreparedStatement statement = this.connection.prepareStatement(formattedSql)) { + statement.setString(1, effectiveSchemaLike); + statement.setString(2, effectiveTableLike); + statement.setString(3, effectiveColumnLike); + try (ResultSet rs = statement.executeQuery()) { + while (rs.next()) { + String cat = rs.getString("cat"); + String sch = rs.getString("sch"); + String tbl = rs.getString("tbl"); + String col = rs.getString("col"); + String typeKind = rs.getString("type_kind"); + String expression = rs.getString("expression"); + String expressionKind = rs.getString("expression_kind"); + String description = rs.getString("description"); + int ordinal = rs.getInt("ordinal"); + List values = + createGraphColumnRow( + cat, sch, tbl, col, typeKind, expression, expressionKind, description, ordinal); + collectedResults.add(FieldValueList.of(values, resultSchemaFields)); + } + } + } catch (SQLException e) { + if (e.getMessage() != null && e.getMessage().contains("Not found")) { + LOG.info( + "Dataset or project not found while querying PROPERTY_GRAPHS for columns in project" + + " '%s'. Skipping.", + project); + } else { + throw e; + } + } + } + } + + List createGraphColumnRow( + String catalog, + String schemaName, + String tableName, + String columnName, + String typeKind, + @Nullable String expression, + @Nullable String expressionKind, + @Nullable String description, + int ordinalPosition) { + ColumnTypeInfo typeInfo = getColumnTypeInfoForTypeKind(typeKind); + + boolean isMeasure = MEASURE_EXPRESSION_KIND.equalsIgnoreCase(expressionKind); + boolean isComputedExpression = + expression != null && !expression.isEmpty() && !expression.equalsIgnoreCase(columnName); + String isGeneratedColumn = (isMeasure || isComputedExpression) ? "YES" : "NO"; + + String remarks; + if (isMeasure) { + String measurePrefix = MEASURE_REMARKS_PREFIX + (expression != null ? expression : ""); + remarks = + (description == null || description.isEmpty()) + ? measurePrefix + : measurePrefix + " - " + description; + } else { + remarks = description; + } + + return buildColumnRowValues( + catalog, + schemaName, + tableName, + columnName, + typeInfo, + DatabaseMetaData.columnNullableUnknown, + remarks, + ordinalPosition, + "", + isGeneratedColumn); + } + + ColumnTypeInfo getColumnTypeInfoForTypeKind(@Nullable String typeKind) { + if (typeKind == null || typeKind.isEmpty()) { + return getColumnTypeInfoForSqlType(null); + } + try { + return getColumnTypeInfoForSqlType( + StandardSQLTypeName.valueOf(typeKind.toUpperCase(Locale.US))); + } catch (IllegalArgumentException e) { + LOG.warning("Unknown BigQuery typeKind '%s' in graph metadata. Mapping to STRING.", typeKind); + return getColumnTypeInfoForSqlType(null); + } + } + + Schema defineGetColumnsSchema() { List fields = new ArrayList<>(24); fields.add( Field.newBuilder("TABLE_CAT", StandardSQLTypeName.STRING) @@ -1899,14 +2024,51 @@ private Schema defineGetColumnsSchema() { List createColumnRow( String catalog, String schemaName, String tableName, Field field, int ordinalPosition) { - List values = new ArrayList<>(24); Field.Mode mode = (field.getMode() == null) ? Field.Mode.NULLABLE : field.getMode(); ColumnTypeInfo typeInfo = mapBigQueryTypeToJdbc(field); - + int nullable = + (mode == Field.Mode.REQUIRED) + ? DatabaseMetaData.columnNoNulls + : DatabaseMetaData.columnNullable; + String isNullable = ""; + switch (mode) { + case REQUIRED: + isNullable = "NO"; + break; + case NULLABLE: + case REPEATED: + isNullable = "YES"; + break; + } + return buildColumnRowValues( + catalog, + schemaName, + tableName, + field.getName(), + typeInfo, + nullable, + field.getDescription(), + ordinalPosition, + isNullable, + "NO"); + } + + private List buildColumnRowValues( + String catalog, + String schemaName, + String tableName, + String columnName, + ColumnTypeInfo typeInfo, + int nullable, + @Nullable String remarks, + int ordinalPosition, + String isNullable, + String isGeneratedColumn) { + List values = new ArrayList<>(24); values.add(createStringFieldValue(catalog)); // 1. TABLE_CAT values.add(createStringFieldValue(schemaName)); // 2. TABLE_SCHEM values.add(createStringFieldValue(tableName)); // 3. TABLE_NAME - values.add(createStringFieldValue(field.getName())); // 4. COLUMN_NAME + values.add(createStringFieldValue(columnName)); // 4. COLUMN_NAME values.add(createLongFieldValue((long) typeInfo.jdbcType)); // 5. DATA_TYPE values.add(createStringFieldValue(typeInfo.typeName)); // 6. TYPE_NAME values.add( @@ -1925,35 +2087,20 @@ List createColumnRow( typeInfo.numPrecRadix == null ? null : typeInfo.numPrecRadix.longValue())); // 10. NUM_PREC_RADIX - int nullable = - (mode == Field.Mode.REQUIRED) - ? DatabaseMetaData.columnNoNulls - : DatabaseMetaData.columnNullable; values.add(createLongFieldValue((long) nullable)); // 11. NULLABLE - values.add(createStringFieldValue(field.getDescription())); // 12. REMARKS + values.add(createStringFieldValue(remarks)); // 12. REMARKS values.add(createNullFieldValue()); // 13. COLUMN_DEF values.add(createNullFieldValue()); // 14. SQL_DATA_TYPE values.add(createNullFieldValue()); // 15. SQL_DATETIME_SUB values.add(createNullFieldValue()); // 16. CHAR_OCTET_LENGTH values.add(createLongFieldValue((long) ordinalPosition)); // 17. ORDINAL_POSITION - String isNullable = ""; - switch (mode) { - case REQUIRED: - isNullable = "NO"; - break; - case NULLABLE: - case REPEATED: - isNullable = "YES"; - break; - } values.add(createStringFieldValue(isNullable)); // 18. IS_NULLABLE values.add(createNullFieldValue()); // 19. SCOPE_CATALOG values.add(createNullFieldValue()); // 20. SCOPE_SCHEMA values.add(createNullFieldValue()); // 21. SCOPE_TABLE values.add(createNullFieldValue()); // 22. SOURCE_DATA_TYPE values.add(createStringFieldValue("NO")); // 23. IS_AUTOINCREMENT - values.add(createStringFieldValue("NO")); // 24. IS_GENERATEDCOLUMN - + values.add(createStringFieldValue(isGeneratedColumn)); // 24. IS_GENERATEDCOLUMN return values; } @@ -5017,6 +5164,13 @@ private static synchronized String getGraphTablesRegionSqlContent() { return graphTablesRegionSqlContent; } + private static synchronized String getGraphColumnsSqlContent() { + if (graphColumnsSqlContent == null) { + graphColumnsSqlContent = readSqlFromFile(GET_GRAPH_COLUMNS_SQL); + } + return graphColumnsSqlContent; + } + static String readSqlFromFile(String filename) { try (InputStream in = BigQueryDatabaseMetaData.class.getResourceAsStream(filename)) { if (in == null) { diff --git a/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetGraphColumns.sql b/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetGraphColumns.sql new file mode 100644 index 000000000000..3479ff0d75cc --- /dev/null +++ b/java-bigquery-jdbc/src/main/resources/com/google/cloud/bigquery/jdbc/DatabaseMetaData_GetGraphColumns.sql @@ -0,0 +1,46 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed 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 + * + * https://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. + */ + +WITH props AS ( + SELECT + property_graph_catalog AS cat, property_graph_schema AS sch, + CONCAT(property_graph_name, '.', JSON_VALUE(t, '$.name')) AS tbl, + JSON_VALUE(p, '$.name') AS col, + JSON_VALUE(p, '$.dataType.typeKind') AS type_kind, + TO_JSON_STRING(JSON_QUERY(p, '$.dataType')) AS data_type_json, + JSON_VALUE(p, '$.expression') AS expression, + JSON_VALUE(p, '$.expressionKind') AS expression_kind, + JSON_VALUE(p, '$.info.description') AS description, + (lp_off * 100000) + p_off AS first_seen + FROM %1$s.%2$s.INFORMATION_SCHEMA.PROPERTY_GRAPHS, + UNNEST([STRUCT(JSON_QUERY_ARRAY(property_graph_metadata_json, '$.nodeTables') AS arr), + STRUCT(JSON_QUERY_ARRAY(property_graph_metadata_json, '$.edgeTables') AS arr)]) k, + UNNEST(k.arr) t, + UNNEST(JSON_QUERY_ARRAY(t, '$.labelAndProperties')) lp WITH OFFSET lp_off, + UNNEST(JSON_QUERY_ARRAY(lp, '$.properties')) p WITH OFFSET p_off + WHERE LOWER(property_graph_schema) LIKE LOWER(?) + AND LOWER(CONCAT(property_graph_name, '.', JSON_VALUE(t, '$.name'))) LIKE LOWER(?) + AND LOWER(JSON_VALUE(p, '$.name')) LIKE LOWER(?) +) +SELECT cat, sch, tbl, col, + ANY_VALUE(type_kind HAVING MIN first_seen) AS type_kind, + ANY_VALUE(data_type_json HAVING MIN first_seen) AS data_type_json, + ANY_VALUE(expression HAVING MIN first_seen) AS expression, + ANY_VALUE(expression_kind HAVING MIN first_seen) AS expression_kind, + ANY_VALUE(description HAVING MIN first_seen) AS description, + ROW_NUMBER() OVER (PARTITION BY cat, sch, tbl ORDER BY MIN(first_seen)) AS ordinal +FROM props +GROUP BY cat, sch, tbl, col \ No newline at end of file diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java index 9c9e5f10b77e..ae2f7692e042 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java @@ -3343,11 +3343,14 @@ public void testMetadataAndResultSetMetadataTypeMappingConsistency(StandardSQLTy } ColumnTypeInfo metadataTypeInfo = dbMetadata.mapBigQueryTypeToJdbc(field); + ColumnTypeInfo graphTypeInfo = dbMetadata.getColumnTypeInfoForTypeKind(type.name()); Integer resultSetType = BigQueryTypeRegistry.toJdbcType(type); assertNotNull(resultSetType, "ResultSet mapping should exist for " + type); assertEquals( metadataTypeInfo.jdbcType, (int) resultSetType, "Type mapping mismatch for " + type); + assertEquals(metadataTypeInfo.jdbcType, graphTypeInfo.jdbcType); + assertEquals(metadataTypeInfo.typeName, graphTypeInfo.typeName); } @Test @@ -3835,6 +3838,120 @@ public void testGetTableTypes_withGraphTablesEnabled() throws SQLException { } } + @Test + public void testShouldSkipGraphColumns() { + when(bigQueryConnection.isEnableGraphMetadata()).thenReturn(false); + assertTrue(dbMetadata.shouldSkipGraphColumns("%")); + assertTrue(dbMetadata.shouldSkipGraphColumns("RetailGraph.Product")); + + when(bigQueryConnection.isEnableGraphMetadata()).thenReturn(true); + assertTrue(dbMetadata.shouldSkipGraphColumns("plainTableWithoutDot")); + assertFalse(dbMetadata.shouldSkipGraphColumns("RetailGraph.Product")); + assertFalse(dbMetadata.shouldSkipGraphColumns("%")); + assertFalse(dbMetadata.shouldSkipGraphColumns("Retail%")); + assertFalse(dbMetadata.shouldSkipGraphColumns("Graph_Node")); + assertFalse(dbMetadata.shouldSkipGraphColumns(null)); + } + + @Test + public void testCreateGraphColumnRow_StandardProperty() { + FieldList schemaFields = dbMetadata.defineGetColumnsSchema().getFields(); + + FieldValueList propRow = + FieldValueList.of( + dbMetadata.createGraphColumnRow( + "test-project", + "my_dataset", + "RetailGraph.Product", + "list_price", + "NUMERIC", + "list_price", + "COLUMN", + "List price in USD", + 1), + schemaFields); + + assertEquals("test-project", propRow.get("TABLE_CAT").getStringValue()); + assertEquals("my_dataset", propRow.get("TABLE_SCHEM").getStringValue()); + assertEquals("RetailGraph.Product", propRow.get("TABLE_NAME").getStringValue()); + assertEquals("list_price", propRow.get("COLUMN_NAME").getStringValue()); + assertEquals(Types.NUMERIC, propRow.get("DATA_TYPE").getLongValue()); + assertEquals("NUMERIC", propRow.get("TYPE_NAME").getStringValue()); + assertEquals(38L, propRow.get("COLUMN_SIZE").getLongValue()); + assertTrue(propRow.get("BUFFER_LENGTH").isNull()); + assertEquals(9L, propRow.get("DECIMAL_DIGITS").getLongValue()); + assertEquals(10L, propRow.get("NUM_PREC_RADIX").getLongValue()); + assertEquals(DatabaseMetaData.columnNullableUnknown, propRow.get("NULLABLE").getLongValue()); + assertEquals("List price in USD", propRow.get("REMARKS").getStringValue()); + assertTrue(propRow.get("COLUMN_DEF").isNull()); + assertEquals(1L, propRow.get("ORDINAL_POSITION").getLongValue()); + assertEquals("", propRow.get("IS_NULLABLE").getStringValue()); + assertEquals("NO", propRow.get("IS_AUTOINCREMENT").getStringValue()); + assertEquals("NO", propRow.get("IS_GENERATEDCOLUMN").getStringValue()); + } + + @Test + public void testCreateGraphColumnRow_MeasureAndComputedProperty() { + FieldList schemaFields = dbMetadata.defineGetColumnsSchema().getFields(); + + // Measure without description + FieldValueList measureRow = + FieldValueList.of( + dbMetadata.createGraphColumnRow( + "test-project", + "my_dataset", + "RetailGraph.Product", + "avg_list_price", + "NUMERIC", + "AVG(list_price)", + "MEASURE", + null, + 2), + schemaFields); + assertEquals(Types.NUMERIC, measureRow.get("DATA_TYPE").getLongValue()); + assertEquals("NUMERIC", measureRow.get("TYPE_NAME").getStringValue()); + assertEquals(38L, measureRow.get("COLUMN_SIZE").getLongValue()); + assertEquals(9L, measureRow.get("DECIMAL_DIGITS").getLongValue()); + assertEquals("MEASURE: AVG(list_price)", measureRow.get("REMARKS").getStringValue()); + assertEquals("YES", measureRow.get("IS_GENERATEDCOLUMN").getStringValue()); + + // Measure with description + FieldValueList measureWithDescRow = + FieldValueList.of( + dbMetadata.createGraphColumnRow( + "test-project", + "my_dataset", + "RetailGraph.Product", + "avg_list_price", + "NUMERIC", + "AVG(list_price)", + "MEASURE", + "Average list price", + 3), + schemaFields); + assertEquals( + "MEASURE: AVG(list_price) - Average list price", + measureWithDescRow.get("REMARKS").getStringValue()); + assertEquals("YES", measureWithDescRow.get("IS_GENERATEDCOLUMN").getStringValue()); + + // Computed property (non-measure, expression differs from column name) + FieldValueList computedRow = + FieldValueList.of( + dbMetadata.createGraphColumnRow( + "test-project", + "my_dataset", + "RetailGraph.Sale", + "line_total", + "NUMERIC", + "quantity * unit_price", + "EXPRESSION", + "Total line amount", + 4), + schemaFields); + assertEquals("Total line amount", computedRow.get("REMARKS").getStringValue()); + assertEquals("YES", computedRow.get("IS_GENERATEDCOLUMN").getStringValue()); + } + @FunctionalInterface private interface MetadataResultSetSupplier { ResultSet get(BigQueryDatabaseMetaData metadata) throws SQLException; diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java index 98f675e16e05..ede93c649b40 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITDatabaseMetadataTest.java @@ -896,6 +896,130 @@ public void testDatabaseMetadataGetTables_PropertyGraph() throws SQLException { } } + @Test + @Tag("disable_tpc") + public void testDatabaseMetadataGetColumns_PropertyGraph() throws SQLException { + String nodeTable = "GRAPH_COL_NODE_TABLE_" + randomNumber; + String edgeTable = "GRAPH_COL_EDGE_TABLE_" + randomNumber; + String graphName = "TEST_GRAPH_GET_COLUMNS_" + randomNumber; + String graphUrl = ITBase.connectionUrl + ";EnableGraphMetadata=1"; + + try (Connection conn = DriverManager.getConnection(graphUrl); + Statement stmt = conn.createStatement()) { + stmt.execute( + String.format( + "CREATE OR REPLACE TABLE `%s.%s.%s` (" + + "id INT64 NOT NULL, " + + "name STRING, " + + "list_price NUMERIC OPTIONS(description='List price in USD'))", + PROJECT_ID, DATASET, nodeTable)); + stmt.execute( + String.format( + "CREATE OR REPLACE TABLE `%s.%s.%s` (" + + "id INT64 NOT NULL, dst_id INT64 NOT NULL, weight INT64)", + PROJECT_ID, DATASET, edgeTable)); + stmt.execute( + String.format( + "CREATE OR REPLACE PROPERTY GRAPH `%s.%s.%s` " + + "NODE TABLES (" + + " `%s.%s.%s` AS Person KEY (id) " + + " LABEL Person PROPERTIES (" + + " id, name, list_price, MEASURE(AVG(list_price)) AS avg_list_price) " + + " LABEL Customer PROPERTIES (id, name), " + + " `%s.%s.%s` AS Tag KEY (id) " + + " LABEL Tag NO PROPERTIES" + + ") " + + "EDGE TABLES (" + + " `%s.%s.%s` AS Knows KEY (id, dst_id) " + + " SOURCE KEY (id) REFERENCES Person (id) " + + " DESTINATION KEY (dst_id) REFERENCES Person (id) " + + " LABEL Knows PROPERTIES (id, dst_id, weight * 2 AS double_weight)" + + ")", + PROJECT_ID, + DATASET, + graphName, + PROJECT_ID, + DATASET, + nodeTable, + PROJECT_ID, + DATASET, + nodeTable, + PROJECT_ID, + DATASET, + edgeTable)); + + DatabaseMetaData metaData = conn.getMetaData(); + + // 1. Node element with deduplicated multi-label properties (id, name) and a measure + try (ResultSet rs = metaData.getColumns(PROJECT_ID, DATASET, graphName + ".Person", "%")) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(PROJECT_ID, rs.getString("TABLE_CAT")); + Assertions.assertEquals(DATASET, rs.getString("TABLE_SCHEM")); + Assertions.assertEquals(graphName + ".Person", rs.getString("TABLE_NAME")); + Assertions.assertEquals("id", rs.getString("COLUMN_NAME")); + Assertions.assertEquals(Types.BIGINT, rs.getInt("DATA_TYPE")); + Assertions.assertEquals("INT64", rs.getString("TYPE_NAME")); + Assertions.assertEquals(DatabaseMetaData.columnNullableUnknown, rs.getInt("NULLABLE")); + Assertions.assertEquals("", rs.getString("IS_NULLABLE")); + Assertions.assertEquals(1, rs.getInt("ORDINAL_POSITION")); + Assertions.assertEquals("NO", rs.getString("IS_AUTOINCREMENT")); + Assertions.assertEquals("NO", rs.getString("IS_GENERATEDCOLUMN")); + + Assertions.assertTrue(rs.next()); + Assertions.assertEquals("name", rs.getString("COLUMN_NAME")); + Assertions.assertEquals(Types.NVARCHAR, rs.getInt("DATA_TYPE")); + Assertions.assertEquals("STRING", rs.getString("TYPE_NAME")); + Assertions.assertEquals(2, rs.getInt("ORDINAL_POSITION")); + Assertions.assertEquals("NO", rs.getString("IS_GENERATEDCOLUMN")); + + Assertions.assertTrue(rs.next()); + Assertions.assertEquals("list_price", rs.getString("COLUMN_NAME")); + Assertions.assertEquals(Types.NUMERIC, rs.getInt("DATA_TYPE")); + Assertions.assertEquals("NUMERIC", rs.getString("TYPE_NAME")); + Assertions.assertEquals(3, rs.getInt("ORDINAL_POSITION")); + Assertions.assertEquals("NO", rs.getString("IS_GENERATEDCOLUMN")); + + Assertions.assertTrue(rs.next()); + Assertions.assertEquals("avg_list_price", rs.getString("COLUMN_NAME")); + Assertions.assertEquals(Types.NUMERIC, rs.getInt("DATA_TYPE")); + Assertions.assertEquals("NUMERIC", rs.getString("TYPE_NAME")); + Assertions.assertEquals(4, rs.getInt("ORDINAL_POSITION")); + Assertions.assertTrue(rs.getString("REMARKS").startsWith("MEASURE: ")); + Assertions.assertEquals("YES", rs.getString("IS_GENERATEDCOLUMN")); + + Assertions.assertFalse(rs.next()); + } + + // 2. Edge element with a calculated property filtered by columnNamePattern + try (ResultSet rs = + metaData.getColumns(PROJECT_ID, DATASET, graphName + ".Knows", "double_weight")) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(graphName + ".Knows", rs.getString("TABLE_NAME")); + Assertions.assertEquals("double_weight", rs.getString("COLUMN_NAME")); + Assertions.assertEquals(Types.BIGINT, rs.getInt("DATA_TYPE")); + Assertions.assertEquals("INT64", rs.getString("TYPE_NAME")); + Assertions.assertEquals("YES", rs.getString("IS_GENERATEDCOLUMN")); + Assertions.assertFalse(rs.next()); + } + + // 3. Element declared with NO PROPERTIES returns 0 rows in getColumns + try (ResultSet rs = metaData.getColumns(PROJECT_ID, DATASET, graphName + ".Tag", "%")) { + Assertions.assertFalse(rs.next()); + } + } finally { + try (Connection cleanupConn = DriverManager.getConnection(ITBase.connectionUrl); + Statement cleanupStmt = cleanupConn.createStatement()) { + cleanupStmt.execute( + String.format( + "DROP PROPERTY GRAPH IF EXISTS `%s.%s.%s`", PROJECT_ID, DATASET, graphName)); + cleanupStmt.execute( + String.format("DROP TABLE IF EXISTS `%s.%s.%s`", PROJECT_ID, DATASET, edgeTable)); + cleanupStmt.execute( + String.format("DROP TABLE IF EXISTS `%s.%s.%s`", PROJECT_ID, DATASET, nodeTable)); + } + } + } + @Test @Tag("disable_tpc") public void testDatabaseMetadataGetSchemas() throws SQLException { From 8a5b9a84ad97c3215db6c84627bf63ddaf079097 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Mon, 2 Nov 2026 15:29:20 -0500 Subject: [PATCH 2/2] address comment --- .../google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index 632ec0df14f0..e7fffeb166d2 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -1882,7 +1882,7 @@ List createGraphColumnRow( int ordinalPosition) { ColumnTypeInfo typeInfo = getColumnTypeInfoForTypeKind(typeKind); - boolean isMeasure = MEASURE_EXPRESSION_KIND.equalsIgnoreCase(expressionKind); + boolean isMeasure = MEASURE_EXPRESSION_KIND.equals(expressionKind); boolean isComputedExpression = expression != null && !expression.isEmpty() && !expression.equalsIgnoreCase(columnName); String isGeneratedColumn = (isMeasure || isComputedExpression) ? "YES" : "NO"; @@ -1916,8 +1916,7 @@ ColumnTypeInfo getColumnTypeInfoForTypeKind(@Nullable String typeKind) { return getColumnTypeInfoForSqlType(null); } try { - return getColumnTypeInfoForSqlType( - StandardSQLTypeName.valueOf(typeKind.toUpperCase(Locale.US))); + return getColumnTypeInfoForSqlType(StandardSQLTypeName.valueOf(typeKind)); } catch (IllegalArgumentException e) { LOG.warning("Unknown BigQuery typeKind '%s' in graph metadata. Mapping to STRING.", typeKind); return getColumnTypeInfoForSqlType(null);