6060import java .util .Optional ;
6161import java .util .Properties ;
6262import java .util .Set ;
63+ import java .util .function .Function ;
6364
6465import static org .apache .flink .connector .jdbc .JdbcConnectionOptions .getBriefAuthProperties ;
6566
@@ -101,33 +102,35 @@ public SpannerCatalog(
101102 String defaultDatabase ,
102103 String baseUrl ,
103104 Properties connectProperties ) {
104- super (userClassLoader , catalogName , defaultDatabase , baseUrl , connectProperties );
105+ // Use the protected constructor to skip validateJdbcUrl, which does not support
106+ // Spanner's deep path URL structure (e.g., /projects/.../databases/).
107+ super (userClassLoader , catalogName , defaultDatabase , baseUrl , connectProperties , true );
105108 this .dialectTypeMapper = new SpannerTypeMapper ();
106109 }
107110
108111 @ Override
109- protected String getDatabaseUrl (String databaseName ) {
110- // Spanner JDBC URL uses semicolon for parameters instead of question mark
111- // Handle semicolon params the same way AbstractJdbcCatalog handles '?' params
112- // Example: "jdbc:cloudspanner://host/projects/.../databases/;autoConfigEmulator=true"
113- // -> "jdbc:cloudspanner://host/projects/.../databases/mydb;autoConfigEmulator=true"
114- int semiColonIndex = baseUrl .indexOf (';' );
115-
112+ protected Function <String , String > calculateUrlFunction (String url ) {
113+ // Spanner JDBC URLs use semicolons for parameters instead of question marks.
114+ // Example: "jdbc:cloudspanner://host/.../databases/;autoConfigEmulator=true"
115+ // -> urlFunction("mydb") -> "jdbc:cloudspanner://host/.../databases/mydb;autoConfigEmulator=true"
116+ int semiColonIndex = url .indexOf (';' );
116117 if (semiColonIndex == -1 ) {
117- // No parameters: traditional baseUrl + databaseName
118- return baseUrl + databaseName ;
119- }
120-
121- // Parameters present: insert database name before ';'
122- String urlWithoutParams = baseUrl .substring (0 , semiColonIndex );
123- String params = baseUrl .substring (semiColonIndex );
124-
125- // Remove trailing '/' from params if AbstractJdbcCatalog added it
126- if (params .endsWith ("/" )) {
127- params = params .substring (0 , params .length () - 1 );
118+ // No semicolon params
119+ String trimmed = url .trim ();
120+ String prefix = trimmed .endsWith ("/" ) ? trimmed : trimmed + "/" ;
121+ return dbName -> prefix + dbName ;
128122 }
123+ // Has semicolon params: split into prefix and params
124+ String urlWithoutParams = url .substring (0 , semiColonIndex );
125+ String params = url .substring (semiColonIndex );
126+ String prefix = urlWithoutParams .endsWith ("/" ) ? urlWithoutParams : urlWithoutParams + "/" ;
127+ return dbName -> prefix + dbName + params ;
128+ }
129129
130- return urlWithoutParams + databaseName + params ;
130+ @ Override
131+ protected void validateConnectionProperties (Properties connectionProperties ) {
132+ // Spanner uses Google Cloud credentials, not traditional username/password.
133+ // Skip the parent's USER_KEY/PASSWORD_KEY validation.
131134 }
132135
133136 private ConnectionOptions getConnectionOptions () {
@@ -137,7 +140,9 @@ private ConnectionOptions getConnectionOptions() {
137140 private SpannerOptions getSpannerOptions (ConnectionOptions options ) {
138141 SpannerOptions .Builder builder =
139142 SpannerOptions .newBuilder ().setProjectId (options .getProjectId ());
140- if (options .getHost ().contains ("localhost" )) {
143+ // Detect the emulator via the driver's own flag (set by ";autoConfigEmulator=true") rather
144+ // than string-matching the host, so emulator hosts other than "localhost" also work.
145+ if (options .isAutoConfigEmulator ()) {
141146 builder .setEmulatorHost (options .getHost ());
142147 } else {
143148 builder .setHost (options .getHost ());
@@ -206,7 +211,8 @@ public CatalogBaseTable getTable(ObjectPath tablePath)
206211 String databaseName = tablePath .getDatabaseName ();
207212
208213 try (Connection conn =
209- DriverManager .getConnection (getDatabaseUrl (databaseName ), connectionProperties )) {
214+ DriverManager .getConnection (
215+ this .urlFunction .apply (databaseName ), connectionProperties )) {
210216 DatabaseMetaData metaData = conn .getMetaData ();
211217 Optional <UniqueConstraint > primaryKey =
212218 getPrimaryKey (
@@ -220,30 +226,33 @@ public CatalogBaseTable getTable(ObjectPath tablePath)
220226 // The INFORMATION_SCHEMA.COLUMNS table is used to retrieve nullability.
221227 Map <String , Boolean > nullables = getColumnNullables (conn , tablePath );
222228
223- PreparedStatement ps =
229+ try ( PreparedStatement ps =
224230 conn .prepareStatement (
225- String .format ("SELECT * FROM %s;" , getSchemaTableName (tablePath )));
226-
227- ResultSetMetaData resultSetMetaData = ps . getMetaData ();
228-
229- String [] columnNames = new String [resultSetMetaData .getColumnCount ()];
230- DataType [] types = new DataType [ resultSetMetaData . getColumnCount ()];
231-
232- for ( int i = 1 ; i < = resultSetMetaData .getColumnCount (); i ++) {
233- final String columnName = resultSetMetaData . getColumnName ( i ) ;
234- columnNames [i - 1 ] = columnName ;
235- types [ i - 1 ] = fromJDBCType ( tablePath , resultSetMetaData , i );
236- if (! nullables . getOrDefault ( columnName , true )) {
237- types [ i - 1 ] = types [ i - 1 ]. notNull ();
231+ String .format ("SELECT * FROM %s;" , getSchemaTableName (tablePath )))) {
232+ ResultSetMetaData resultSetMetaData = ps . getMetaData ();
233+
234+ String [] columnNames = new String [ resultSetMetaData . getColumnCount ()];
235+ DataType [] types = new DataType [resultSetMetaData .getColumnCount ()];
236+
237+ for ( int i = 1 ; i <= resultSetMetaData . getColumnCount (); i ++) {
238+ final String columnName = resultSetMetaData .getColumnName ( i );
239+ columnNames [ i - 1 ] = columnName ;
240+ types [i - 1 ] = fromJDBCType ( tablePath , resultSetMetaData , i ) ;
241+ if (! nullables . getOrDefault ( columnName , true )) {
242+ types [ i - 1 ] = types [ i - 1 ]. notNull ();
243+ }
238244 }
239- }
240245
241- Schema .Builder schemaBuilder = Schema .newBuilder ().fromFields (columnNames , types );
242- primaryKey .ifPresent (
243- pk -> schemaBuilder .primaryKeyNamed (pk .getName (), pk .getColumns ()));
244- Schema tableSchema = schemaBuilder .build ();
246+ Schema .Builder schemaBuilder = Schema .newBuilder ().fromFields (columnNames , types );
247+ primaryKey .ifPresent (
248+ pk -> schemaBuilder .primaryKeyNamed (pk .getName (), pk .getColumns ()));
249+ Schema tableSchema = schemaBuilder .build ();
245250
246- return CatalogTable .of (tableSchema , null , Lists .newArrayList (), getOptions (tablePath ));
251+ return CatalogTable .newBuilder ()
252+ .schema (tableSchema )
253+ .options (getOptions (tablePath ))
254+ .build ();
255+ }
247256 } catch (Exception e ) {
248257 throw new CatalogException (
249258 String .format ("Failed getting table %s" , tablePath .getFullName ()), e );
@@ -281,7 +290,7 @@ public List<String> listTables(String databaseName)
281290 throw new DatabaseNotExistException (getName (), databaseName );
282291 }
283292
284- final String url = getDatabaseUrl (databaseName );
293+ final String url = this . urlFunction . apply (databaseName );
285294 try (Connection conn = DriverManager .getConnection (url , connectionProperties )) {
286295 // get all schemas
287296 List <String > schemas ;
0 commit comments