From caffda03e528da6a3c2c17b7058eb5d29f5086f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 24 Feb 2022 19:47:02 +0100 Subject: [PATCH] fix: create specific metadata queries for PG (#759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: create specific metadata queries for PG Creates specific metadata queries for PostgreSQL dialect databases and removes the use of the /*GSQL*/ header in the existing metadata queries. * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: linting Co-authored-by: Owl Bot --- .../spanner/jdbc/JdbcDatabaseMetaData.java | 30 +- .../jdbc/DatabaseMetaData_GetColumns.sql | 1 - .../DatabaseMetaData_GetCrossReferences.sql | 1 - .../jdbc/DatabaseMetaData_GetExportedKeys.sql | 1 - .../jdbc/DatabaseMetaData_GetImportedKeys.sql | 1 - .../jdbc/DatabaseMetaData_GetIndexInfo.sql | 1 - .../jdbc/DatabaseMetaData_GetPrimaryKeys.sql | 1 - .../jdbc/DatabaseMetaData_GetSchemas.sql | 1 - .../jdbc/DatabaseMetaData_GetTables.sql | 1 - .../DatabaseMetaData_GetColumns.sql | 81 ++ .../DatabaseMetaData_GetCrossReferences.sql | 31 + .../DatabaseMetaData_GetExportedKeys.sql | 31 + .../DatabaseMetaData_GetImportedKeys.sql | 31 + .../DatabaseMetaData_GetIndexInfo.sql | 36 + .../DatabaseMetaData_GetPrimaryKeys.sql | 29 + .../DatabaseMetaData_GetSchemas.sql | 21 + .../postgresql/DatabaseMetaData_GetTables.sql | 30 + .../spanner/jdbc/ITAbstractJdbcTest.java | 19 +- ...cDatabaseMetaDataWithMockedServerTest.java | 118 +-- .../jdbc/it/ITJdbcPgDatabaseMetaDataTest.java | 808 ++++++++++++++++++ .../spanner/jdbc/it/CreateMusicTables_PG.sql | 74 +- 21 files changed, 1186 insertions(+), 161 deletions(-) create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetColumns.sql create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetIndexInfo.sql create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetPrimaryKeys.sql create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetSchemas.sql create mode 100644 src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetTables.sql create mode 100644 src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java index 42828fb5..66c76b53 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java @@ -19,6 +19,7 @@ import com.google.auth.Credentials; import com.google.auth.ServiceAccountSigner; import com.google.auth.oauth2.UserCredentials; +import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ResultSets; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; @@ -48,8 +49,16 @@ class JdbcDatabaseMetaData extends AbstractJdbcWrapper implements DatabaseMetaDa private static final String PRODUCT_NAME = "Google Cloud Spanner"; @VisibleForTesting - static String readSqlFromFile(String filename) { - InputStream in = JdbcDatabaseMetaData.class.getResourceAsStream(filename); + static String readSqlFromFile(String filename, Dialect dialect) { + InputStream in; + switch (dialect) { + case POSTGRESQL: + in = JdbcDatabaseMetaData.class.getResourceAsStream("postgresql/" + filename); + break; + case GOOGLE_STANDARD_SQL: + default: + in = JdbcDatabaseMetaData.class.getResourceAsStream(filename); + } BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder builder = new StringBuilder(); try (Scanner scanner = new Scanner(reader)) { @@ -743,7 +752,7 @@ private JdbcPreparedStatement prepareStatementReplaceNullWithAnyString( public ResultSet getTables( String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException { - String sql = readSqlFromFile("DatabaseMetaData_GetTables.sql"); + String sql = readSqlFromFile("DatabaseMetaData_GetTables.sql", connection.getDialect()); String type1; String type2; if (types == null || types.length == 0) { @@ -789,7 +798,7 @@ public ResultSet getTableTypes() { public ResultSet getColumns( String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - String sql = readSqlFromFile("DatabaseMetaData_GetColumns.sql"); + String sql = readSqlFromFile("DatabaseMetaData_GetColumns.sql", connection.getDialect()); JdbcPreparedStatement statement = prepareStatementReplaceNullWithAnyString( sql, catalog, schemaPattern, tableNamePattern, columnNamePattern); @@ -858,7 +867,7 @@ private ResultSet getEmptyColumnsResultSet() { @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { JdbcPreconditions.checkArgument(table != null, "table may not be null"); - String sql = readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql"); + String sql = readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql", connection.getDialect()); JdbcPreparedStatement statement = prepareStatementReplaceNullWithAnyString(sql, catalog, schema, table); return statement.executeQueryWithOptions(InternalMetadataQuery.INSTANCE); @@ -868,7 +877,7 @@ public ResultSet getPrimaryKeys(String catalog, String schema, String table) thr public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { JdbcPreconditions.checkArgument(table != null, "table may not be null"); - String sql = readSqlFromFile("DatabaseMetaData_GetImportedKeys.sql"); + String sql = readSqlFromFile("DatabaseMetaData_GetImportedKeys.sql", connection.getDialect()); JdbcPreparedStatement statement = prepareStatementReplaceNullWithAnyString(sql, catalog, schema, table); return statement.executeQueryWithOptions(InternalMetadataQuery.INSTANCE); @@ -878,7 +887,7 @@ public ResultSet getImportedKeys(String catalog, String schema, String table) public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { JdbcPreconditions.checkArgument(table != null, "table may not be null"); - String sql = readSqlFromFile("DatabaseMetaData_GetExportedKeys.sql"); + String sql = readSqlFromFile("DatabaseMetaData_GetExportedKeys.sql", connection.getDialect()); JdbcPreparedStatement statement = prepareStatementReplaceNullWithAnyString(sql, catalog, schema, table); return statement.executeQueryWithOptions(InternalMetadataQuery.INSTANCE); @@ -893,7 +902,8 @@ public ResultSet getCrossReference( String foreignSchema, String foreignTable) throws SQLException { - String sql = readSqlFromFile("DatabaseMetaData_GetCrossReferences.sql"); + String sql = + readSqlFromFile("DatabaseMetaData_GetCrossReferences.sql", connection.getDialect()); JdbcPreparedStatement statement = prepareStatementReplaceNullWithAnyString( sql, @@ -1251,7 +1261,7 @@ public ResultSet getIndexInfo(String catalog, String schema, String indexName) private ResultSet getIndexInfo( String catalog, String schema, String table, String indexName, boolean unique) throws SQLException { - String sql = readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql"); + String sql = readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql", connection.getDialect()); JdbcPreparedStatement statement = prepareStatementReplaceNullWithAnyString( sql, catalog, schema, table, indexName, unique ? "YES" : "%"); @@ -1467,7 +1477,7 @@ public RowIdLifetime getRowIdLifetime() { @Override public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { - String sql = readSqlFromFile("DatabaseMetaData_GetSchemas.sql"); + String sql = readSqlFromFile("DatabaseMetaData_GetSchemas.sql", connection.getDialect()); JdbcPreparedStatement statement = prepareStatementReplaceNullWithAnyString(sql, catalog, schemaPattern); return statement.executeQueryWithOptions(InternalMetadataQuery.INSTANCE); diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql index 3ee102a8..e5226a83 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql index d5c1620c..7d362ca5 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql index ba6630c4..631886d7 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql index 4bc9296f..50994a55 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql index 5fddb8b0..e0b2a2f6 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql index 455c474c..cec8e7e0 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql index 3903236b..1e302623 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql index 8b3083bd..1d485581 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql @@ -1,4 +1,3 @@ -/*GSQL*/ /* * Copyright 2019 Google LLC * diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetColumns.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetColumns.sql new file mode 100644 index 00000000..c932e667 --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetColumns.sql @@ -0,0 +1,81 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT TABLE_CATALOG AS "TABLE_CAT", TABLE_SCHEMA AS "TABLE_SCHEM", TABLE_NAME AS "TABLE_NAME", COLUMN_NAME AS "COLUMN_NAME", + CASE + WHEN DATA_TYPE LIKE 'ARRAY' THEN 2003 + WHEN DATA_TYPE = 'boolean' THEN 16 + WHEN DATA_TYPE LIKE 'bytea' THEN -2 + WHEN DATA_TYPE = 'date' THEN 91 + WHEN DATA_TYPE = 'double precision' THEN 8 + WHEN DATA_TYPE = 'bigint' THEN -5 + WHEN DATA_TYPE = 'numeric' THEN 2 + WHEN DATA_TYPE LIKE 'character varying' THEN -9 + WHEN DATA_TYPE = 'jsonb' THEN -9 + WHEN DATA_TYPE = 'timestamp with time zone' THEN 93 + END AS "DATA_TYPE", + DATA_TYPE AS "TYPE_NAME", + CASE + WHEN DATA_TYPE LIKE 'ARRAY' THEN 0 + WHEN DATA_TYPE = 'boolean' THEN NULL + WHEN DATA_TYPE LIKE 'bytea' THEN 10485760 + WHEN DATA_TYPE = 'date' THEN 10 + WHEN DATA_TYPE = 'double precision' THEN 15 + WHEN DATA_TYPE = 'bigint' THEN 19 + WHEN DATA_TYPE = 'numeric' THEN 15 + WHEN DATA_TYPE LIKE 'character varying' THEN CHARACTER_MAXIMUM_LENGTH + WHEN DATA_TYPE = 'jsonb' THEN 2621440 + WHEN DATA_TYPE = 'timestamp with time zone' THEN 35 + END AS "COLUMN_SIZE", + 0 AS "BUFFER_LENGTH", + CASE + WHEN DATA_TYPE LIKE 'double precision' THEN 16 + WHEN DATA_TYPE LIKE 'numeric' THEN 16383 + ELSE NULL + END AS "DECIMAL_DIGITS", + CASE + WHEN DATA_TYPE LIKE 'bigint' THEN 10 + WHEN DATA_TYPE LIKE 'numeric' THEN 10 + WHEN DATA_TYPE LIKE 'double precision' THEN 2 + ELSE NULL + END AS "NUM_PREC_RADIX", + CASE + WHEN IS_NULLABLE = 'YES' THEN 1 + WHEN IS_NULLABLE = 'NO' THEN 0 + ELSE 2 + END AS "NULLABLE", + NULL AS "REMARKS", + NULL AS "COLUMN_DEF", + 0 AS "SQL_DATA_TYPE", + 0 AS "SQL_DATETIME_SUB", + CHARACTER_MAXIMUM_LENGTH AS "CHAR_OCTET_LENGTH", + ORDINAL_POSITION AS "ORDINAL_POSITION", + IS_NULLABLE AS "IS_NULLABLE", + NULL AS "SCOPE_CATALOG", + NULL AS "SCOPE_SCHEMA", + NULL AS "SCOPE_TABLE", + NULL AS "SOURCE_DATA_TYPE", + 'NO' AS "IS_AUTOINCREMENT", + CASE + WHEN (IS_GENERATED = 'NEVER') THEN 'NO' + ELSE 'YES' + END AS "IS_GENERATEDCOLUMN" +FROM INFORMATION_SCHEMA.COLUMNS C +WHERE UPPER(C.TABLE_CATALOG) LIKE ? + AND UPPER(C.TABLE_SCHEMA) LIKE ? + AND UPPER(C.TABLE_NAME) LIKE ? + AND UPPER(C.COLUMN_NAME) LIKE ? +ORDER BY TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql new file mode 100644 index 00000000..d7542ca6 --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetCrossReferences.sql @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT PARENT.TABLE_CATALOG AS "PKTABLE_CAT", PARENT.TABLE_SCHEMA AS "PKTABLE_SCHEM", PARENT.TABLE_NAME AS "PKTABLE_NAME", + PARENT.COLUMN_NAME AS "PKCOLUMN_NAME", CHILD.TABLE_CATALOG AS "FKTABLE_CAT", CHILD.TABLE_SCHEMA AS "FKTABLE_SCHEM", + CHILD.TABLE_NAME AS "FKTABLE_NAME", CHILD.COLUMN_NAME AS "FKCOLUMN_NAME", CHILD.ORDINAL_POSITION AS "KEY_SEQ", 3 AS "UPDATE_RULE", + 3 AS "DELETE_RULE", CONSTRAINTS.CONSTRAINT_NAME AS "FK_NAME", CONSTRAINTS.UNIQUE_CONSTRAINT_NAME AS "PK_NAME", + 7 AS "DEFERRABILITY" +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS + INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD ON CONSTRAINTS.CONSTRAINT_CATALOG=CHILD.CONSTRAINT_CATALOG AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME + INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG=PARENT.CONSTRAINT_CATALOG AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT +WHERE UPPER(PARENT.TABLE_CATALOG) LIKE ? + AND UPPER(PARENT.TABLE_SCHEMA) LIKE ? + AND UPPER(PARENT.TABLE_NAME) LIKE ? + AND UPPER(CHILD.TABLE_CATALOG) LIKE ? + AND UPPER(CHILD.TABLE_SCHEMA) LIKE ? + AND UPPER(CHILD.TABLE_NAME) LIKE ? +ORDER BY CHILD.TABLE_CATALOG, CHILD.TABLE_SCHEMA, CHILD.TABLE_NAME, CHILD.ORDINAL_POSITION diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql new file mode 100644 index 00000000..7c04349e --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetExportedKeys.sql @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT PARENT.TABLE_CATALOG AS "PKTABLE_CAT", PARENT.TABLE_SCHEMA AS "PKTABLE_SCHEM", PARENT.TABLE_NAME AS "PKTABLE_NAME", + PARENT.COLUMN_NAME AS "PKCOLUMN_NAME", CHILD.TABLE_CATALOG AS "FKTABLE_CAT", CHILD.TABLE_SCHEMA AS "FKTABLE_SCHEM", + CHILD.TABLE_NAME AS "FKTABLE_NAME", CHILD.COLUMN_NAME AS "FKCOLUMN_NAME", + CHILD.ORDINAL_POSITION AS "KEY_SEQ", + 1 AS "UPDATE_RULE", -- 1 = importedKeyRestrict + 1 AS "DELETE_RULE", -- 1 = importedKeyRestrict + CONSTRAINTS.CONSTRAINT_NAME AS "FK_NAME", CONSTRAINTS.UNIQUE_CONSTRAINT_NAME AS "PK_NAME", + 7 AS "DEFERRABILITY" -- 7 = importedKeyNotDeferrable +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS + INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD ON CONSTRAINTS.CONSTRAINT_CATALOG=CHILD.CONSTRAINT_CATALOG AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME + INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG=PARENT.CONSTRAINT_CATALOG AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT +WHERE UPPER(PARENT.TABLE_CATALOG) LIKE ? + AND UPPER(PARENT.TABLE_SCHEMA) LIKE ? + AND UPPER(PARENT.TABLE_NAME) LIKE ? +ORDER BY CHILD.TABLE_CATALOG, CHILD.TABLE_SCHEMA, CHILD.TABLE_NAME, CHILD.ORDINAL_POSITION diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql new file mode 100644 index 00000000..44481deb --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetImportedKeys.sql @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT PARENT.TABLE_CATALOG AS "PKTABLE_CAT", PARENT.TABLE_SCHEMA AS "PKTABLE_SCHEM", PARENT.TABLE_NAME AS "PKTABLE_NAME", + PARENT.COLUMN_NAME AS "PKCOLUMN_NAME", CHILD.TABLE_CATALOG AS "FKTABLE_CAT", CHILD.TABLE_SCHEMA AS "FKTABLE_SCHEM", + CHILD.TABLE_NAME AS "FKTABLE_NAME", CHILD.COLUMN_NAME AS "FKCOLUMN_NAME", + CHILD.ORDINAL_POSITION AS "KEY_SEQ", + 1 AS "UPDATE_RULE", -- 1 = importedKeyRestrict + 1 AS "DELETE_RULE", -- 1 = importedKeyRestrict + CONSTRAINTS.CONSTRAINT_NAME AS "FK_NAME", CONSTRAINTS.UNIQUE_CONSTRAINT_NAME AS "PK_NAME", + 7 AS "DEFERRABILITY" -- 7 = importedKeyNotDeferrable +FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS + INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD ON CONSTRAINTS.CONSTRAINT_CATALOG=CHILD.CONSTRAINT_CATALOG AND CONSTRAINTS.CONSTRAINT_SCHEMA= CHILD.CONSTRAINT_SCHEMA AND CONSTRAINTS.CONSTRAINT_NAME= CHILD.CONSTRAINT_NAME + INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT ON CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG=PARENT.CONSTRAINT_CATALOG AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA=PARENT.CONSTRAINT_SCHEMA AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME=PARENT.CONSTRAINT_NAME AND PARENT.ORDINAL_POSITION=CHILD.POSITION_IN_UNIQUE_CONSTRAINT +WHERE UPPER(CHILD.TABLE_CATALOG) LIKE ? + AND UPPER(CHILD.TABLE_SCHEMA) LIKE ? + AND UPPER(CHILD.TABLE_NAME) LIKE ? +ORDER BY PARENT.TABLE_CATALOG, PARENT.TABLE_SCHEMA, PARENT.TABLE_NAME, CHILD.ORDINAL_POSITION diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetIndexInfo.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetIndexInfo.sql new file mode 100644 index 00000000..7bffd40b --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetIndexInfo.sql @@ -0,0 +1,36 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT IDX.TABLE_CATALOG AS "TABLE_CAT", IDX.TABLE_SCHEMA AS "TABLE_SCHEM", IDX.TABLE_NAME AS "TABLE_NAME", + CASE WHEN IS_UNIQUE='YES' THEN FALSE ELSE TRUE END AS "NON_UNIQUE", + IDX.TABLE_CATALOG AS "INDEX_QUALIFIER", IDX.INDEX_NAME AS "INDEX_NAME", + 2 AS "TYPE", + ORDINAL_POSITION AS "ORDINAL_POSITION", COLUMN_NAME AS "COLUMN_NAME", SUBSTR(COLUMN_ORDERING, 1, 1) AS "ASC_OR_DESC", + -1 AS "CARDINALITY", -- Not supported + -1 AS "PAGES", -- Not supported + NULL AS "FILTER_CONDITION" +FROM INFORMATION_SCHEMA.INDEXES IDX +INNER JOIN INFORMATION_SCHEMA.INDEX_COLUMNS COL + ON IDX.TABLE_CATALOG=COL.TABLE_CATALOG + AND IDX.TABLE_SCHEMA=COL.TABLE_SCHEMA + AND IDX.TABLE_NAME=COL.TABLE_NAME + AND IDX.INDEX_NAME=COL.INDEX_NAME +WHERE UPPER(IDX.TABLE_CATALOG) LIKE ? + AND UPPER(IDX.TABLE_SCHEMA) LIKE ? + AND UPPER(IDX.TABLE_NAME) LIKE ? + AND UPPER(IDX.INDEX_NAME) LIKE ? + AND UPPER(IS_UNIQUE) LIKE ? +ORDER BY IDX.TABLE_NAME, IS_UNIQUE DESC, IDX.INDEX_NAME, CASE WHEN ORDINAL_POSITION IS NULL THEN 0 ELSE ORDINAL_POSITION END diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetPrimaryKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetPrimaryKeys.sql new file mode 100644 index 00000000..de9214c6 --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetPrimaryKeys.sql @@ -0,0 +1,29 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT IDX.TABLE_CATALOG AS "TABLE_CAT", IDX.TABLE_SCHEMA AS "TABLE_SCHEM", IDX.TABLE_NAME AS "TABLE_NAME", + COLS.COLUMN_NAME AS "COLUMN_NAME", ORDINAL_POSITION AS "KEY_SEQ", IDX.INDEX_NAME AS "PK_NAME" +FROM INFORMATION_SCHEMA.INDEXES IDX +INNER JOIN INFORMATION_SCHEMA.INDEX_COLUMNS COLS + ON IDX.TABLE_CATALOG=COLS.TABLE_CATALOG + AND IDX.TABLE_SCHEMA=COLS.TABLE_SCHEMA + AND IDX.TABLE_NAME=COLS.TABLE_NAME + AND IDX.INDEX_NAME=COLS.INDEX_NAME +WHERE IDX.INDEX_TYPE='PRIMARY_KEY' + AND UPPER(IDX.TABLE_CATALOG) LIKE ? + AND UPPER(IDX.TABLE_SCHEMA) LIKE ? + AND UPPER(IDX.TABLE_NAME) LIKE ? +ORDER BY COLS.ORDINAL_POSITION \ No newline at end of file diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetSchemas.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetSchemas.sql new file mode 100644 index 00000000..5b93ef43 --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetSchemas.sql @@ -0,0 +1,21 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT SCHEMA_NAME AS "TABLE_SCHEM", CATALOG_NAME AS "TABLE_CATALOG" +FROM INFORMATION_SCHEMA.SCHEMATA +WHERE UPPER(CATALOG_NAME) LIKE ? + AND UPPER(SCHEMA_NAME) LIKE ? +ORDER BY CATALOG_NAME, SCHEMA_NAME diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetTables.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetTables.sql new file mode 100644 index 00000000..911616cf --- /dev/null +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetTables.sql @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +SELECT TABLE_CATALOG AS "TABLE_CAT", TABLE_SCHEMA AS "TABLE_SCHEM", TABLE_NAME AS "TABLE_NAME", + CASE WHEN TABLE_TYPE = 'BASE TABLE' THEN 'TABLE' ELSE TABLE_TYPE END AS "TABLE_TYPE", + NULL AS "REMARKS", NULL AS "TYPE_CAT", NULL AS "TYPE_SCHEM", NULL AS "TYPE_NAME", + NULL AS "SELF_REFERENCING_COL_NAME", NULL AS "REF_GENERATION" +FROM INFORMATION_SCHEMA.TABLES AS T +WHERE UPPER(TABLE_CATALOG) LIKE ? + AND UPPER(TABLE_SCHEMA) LIKE ? + AND UPPER(TABLE_NAME) LIKE ? + AND ( + (CASE WHEN TABLE_TYPE = 'BASE TABLE' THEN 'TABLE' ELSE TABLE_TYPE END) LIKE ? + OR + (CASE WHEN TABLE_TYPE = 'BASE TABLE' THEN 'TABLE' ELSE TABLE_TYPE END) LIKE ? + ) +ORDER BY TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java index 2ab75e59..1a8a6ade 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java @@ -129,7 +129,7 @@ public CloudSpannerJdbcConnection createConnection(Dialect dialect) throws SQLEx } public CloudSpannerJdbcConnection createConnection() throws SQLException { - return createConnection(Dialect.GOOGLE_STANDARD_SQL); + return createConnection(Dialect.POSTGRESQL); } protected void appendConnectionUri(StringBuilder uri) {} @@ -183,6 +183,20 @@ public Dialect getDialect() { return Dialect.GOOGLE_STANDARD_SQL; } + public String getDefaultCatalog() { + if (getDialect() == Dialect.POSTGRESQL) { + return getDatabase(Dialect.POSTGRESQL).getId().getDatabase(); + } + return ""; + } + + public String getDefaultSchema() { + if (getDialect() == Dialect.POSTGRESQL) { + return "public"; + } + return ""; + } + @Before public void createMusicTables() throws SQLException { if (doCreateMusicTables()) { @@ -206,7 +220,8 @@ public void createMusicTables() throws SQLException { } protected boolean tableExists(Connection connection, String table) throws SQLException { - try (ResultSet rs = connection.getMetaData().getTables("", "", table, null)) { + try (ResultSet rs = + connection.getMetaData().getTables(getDefaultCatalog(), getDefaultSchema(), table, null)) { if (rs.next()) { if (rs.getString("TABLE_NAME").equalsIgnoreCase(table)) { return true; diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java index f9011e4f..0300e127 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java @@ -16,8 +16,6 @@ package com.google.cloud.spanner.jdbc; -import static com.google.common.truth.Truth.assertThat; - import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.MockSpannerServiceImpl; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; @@ -72,13 +70,6 @@ public class JdbcDatabaseMetaDataWithMockedServerTest { .setMetadata(RESULTSET_METADATA) .build(); - private static final String GSQL_STATEMENT = "/*GSQL*/"; - - /* Checks if the SQL statement starts with /*GSQL*/ - private boolean isGoogleSql(String sql) { - return sql.startsWith(GSQL_STATEMENT); - } - @Parameter public Dialect dialect; @Parameters(name = "dialect = {0}") @@ -133,7 +124,7 @@ private Connection createConnection() throws SQLException { public void getTablesInDdlBatch() throws SQLException { String sql = parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql")); + JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql", dialect)); ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql); mockSpanner.putStatementResult( StatementResult.query( @@ -167,7 +158,7 @@ public void getTablesInDdlBatch() throws SQLException { public void getColumnsInDdlBatch() throws SQLException { String sql = parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetColumns.sql")); + JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetColumns.sql", dialect)); ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql); mockSpanner.putStatementResult( StatementResult.query( @@ -201,7 +192,8 @@ public void getKeysInDdlBatch() throws SQLException { "DatabaseMetaData_GetImportedKeys.sql", "DatabaseMetaData_GetExportedKeys.sql" }) { - String sql = parser.removeCommentsAndTrim(JdbcDatabaseMetaData.readSqlFromFile(fileName)); + String sql = + parser.removeCommentsAndTrim(JdbcDatabaseMetaData.readSqlFromFile(fileName, dialect)); ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql); mockSpanner.putStatementResult( StatementResult.query( @@ -238,7 +230,8 @@ public void getKeysInDdlBatch() throws SQLException { public void getCrossReferencesInDdlBatch() throws SQLException { String sql = parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetCrossReferences.sql")); + JdbcDatabaseMetaData.readSqlFromFile( + "DatabaseMetaData_GetCrossReferences.sql", dialect)); ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql); mockSpanner.putStatementResult( StatementResult.query( @@ -273,7 +266,7 @@ public void getCrossReferencesInDdlBatch() throws SQLException { public void getIndexInfoInDdlBatch() throws SQLException { String sql = parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql")); + JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql", dialect)); ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql); mockSpanner.putStatementResult( StatementResult.query( @@ -306,7 +299,7 @@ public void getIndexInfoInDdlBatch() throws SQLException { public void getSchemasInDdlBatch() throws SQLException { String sql = parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetSchemas.sql")); + JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetSchemas.sql", dialect)); ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql); mockSpanner.putStatementResult( StatementResult.query( @@ -331,99 +324,4 @@ public void getSchemasInDdlBatch() throws SQLException { connection.createStatement().execute("ABORT BATCH"); } } - - @Test - public void verifyGoogleSqlHeaderIsCorrectlyParsed() { - // Verify that the `/*GSQL*/` header is kept in the SQL statement without comments if the - // dialect is PostgreSQL, and that it is removed if the dialect is Google_Standard_Sql. - if (dialect == Dialect.POSTGRESQL) { - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile( - "DatabaseMetaData_GetCrossReferences.sql")))) - .isTrue(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile( - "DatabaseMetaData_GetImportedKeys.sql")))) - .isTrue(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql")))) - .isTrue(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql")))) - .isTrue(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetColumns.sql")))) - .isTrue(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile( - "DatabaseMetaData_GetExportedKeys.sql")))) - .isTrue(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql")))) - .isTrue(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetSchemas.sql")))) - .isTrue(); - } else { - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile( - "DatabaseMetaData_GetCrossReferences.sql")))) - .isFalse(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile( - "DatabaseMetaData_GetImportedKeys.sql")))) - .isFalse(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql")))) - .isFalse(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql")))) - .isFalse(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetColumns.sql")))) - .isFalse(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile( - "DatabaseMetaData_GetExportedKeys.sql")))) - .isFalse(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql")))) - .isFalse(); - assertThat( - isGoogleSql( - parser.removeCommentsAndTrim( - JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetSchemas.sql")))) - .isFalse(); - } - } } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java new file mode 100644 index 00000000..e9b682a2 --- /dev/null +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java @@ -0,0 +1,808 @@ +/* + * Copyright 2022 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 + * + * http://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. + */ + +package com.google.cloud.spanner.jdbc.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; +import com.google.cloud.spanner.testing.EmulatorSpannerHelper; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.List; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for {@link DatabaseMetaData} implementation for Spangres. */ +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITJdbcPgDatabaseMetaDataTest extends ITAbstractJdbcTest { + private static final String DEFAULT_SCHEMA = "public"; + private static final String SINGERS_TABLE = "Singers"; + private static final String ALBUMS_TABLE = "Albums"; + private static final String SONGS_TABLE = "Songs"; + private static final String CONCERTS_TABLE = "Concerts"; + private static final String TABLE_WITH_ALL_COLS = "TableWithAllColumnTypes"; + private static final String TABLE_WITH_REF = "TableWithRef"; + + @BeforeClass + public static void skipOnEmulator() { + assumeFalse( + "PostgreSQL dialect is not yet supported on the emulator", + EmulatorSpannerHelper.isUsingEmulator()); + } + + @Override + protected boolean doCreateMusicTables() { + return true; + } + + private static final class Column { + private final String name; + private final int type; + private final String typeName; + private final Integer colSize; + private final Integer decimalDigits; + private final Integer radix; + private final boolean nullable; + private final Integer charOctetLength; + private final boolean computed; + + private Column( + String name, + int type, + String typeName, + Integer colSize, + Integer decimalDigits, + Integer radix, + boolean nullable, + Integer charOctetLength) { + this(name, type, typeName, colSize, decimalDigits, radix, nullable, charOctetLength, false); + } + + private Column( + String name, + int type, + String typeName, + Integer colSize, + Integer decimalDigits, + Integer radix, + boolean nullable, + Integer charOctetLength, + boolean computed) { + this.name = name; + this.type = type; + this.typeName = typeName; + this.colSize = colSize; + this.decimalDigits = decimalDigits; + this.radix = radix; + this.nullable = nullable; + this.charOctetLength = charOctetLength; + this.computed = computed; + } + } + + private static final List EXPECTED_COLUMNS = + Arrays.asList( + new Column("colint64", Types.BIGINT, "bigint", 19, null, 10, false, null), + new Column("colfloat64", Types.DOUBLE, "double precision", 15, 16, 2, false, null), + new Column("colbool", Types.BOOLEAN, "boolean", null, null, null, false, null), + new Column("colstring", Types.NVARCHAR, "character varying", 100, null, null, false, 100), + new Column( + "colstringmax", Types.NVARCHAR, "character varying", null, null, null, true, null), + new Column("colbytes", Types.BINARY, "bytea", 10485760, null, null, false, null), + new Column( + "coltimestamp", + Types.TIMESTAMP, + "timestamp with time zone", + 35, + null, + null, + true, + null), + new Column("colnumeric", Types.NUMERIC, "numeric", 15, 16383, 10, false, null)); + + public Dialect getDialect() { + return Dialect.POSTGRESQL; + } + + @Test + public void testGetColumns() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection + .getMetaData() + .getColumns(getDefaultCatalog(), DEFAULT_SCHEMA, TABLE_WITH_ALL_COLS, null)) { + int pos = 1; + for (Column col : EXPECTED_COLUMNS) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("TABLE_SCHEM")); + assertEquals("tablewithallcolumntypes", rs.getString("TABLE_NAME")); + assertEquals(col.name, rs.getString("COLUMN_NAME")); + assertEquals(col.type, rs.getInt("DATA_TYPE")); + assertEquals(col.typeName, rs.getString("TYPE_NAME")); + if (col.colSize == null) { + assertEquals(0, rs.getInt("COLUMN_SIZE")); + assertTrue(rs.wasNull()); + } else { + assertEquals(col.colSize.intValue(), rs.getInt("COLUMN_SIZE")); + } + rs.getObject("BUFFER_LENGTH"); // just assert that it exists + if (col.decimalDigits == null) { + assertEquals(0, rs.getInt("DECIMAL_DIGITS")); + assertTrue(rs.wasNull()); + } else { + assertEquals(col.decimalDigits.intValue(), rs.getInt("DECIMAL_DIGITS")); + } + if (col.radix == null) { + assertEquals(0, rs.getInt("NUM_PREC_RADIX")); + assertTrue(rs.wasNull()); + } else { + assertEquals(col.radix.intValue(), rs.getInt("NUM_PREC_RADIX")); + } + assertEquals( + "Nullable difference for " + col.name, + col.nullable ? DatabaseMetaData.columnNullable : DatabaseMetaData.columnNoNulls, + rs.getInt("NULLABLE")); + assertNull(rs.getString("REMARKS")); + assertNull(rs.getString("COLUMN_DEF")); + assertEquals(0, rs.getInt("SQL_DATA_TYPE")); + assertEquals(0, rs.getInt("SQL_DATETIME_SUB")); + if (col.charOctetLength == null) { + assertEquals(0, rs.getInt("CHAR_OCTET_LENGTH")); + assertTrue(rs.wasNull()); + } else { + assertEquals(col.charOctetLength.intValue(), rs.getInt("CHAR_OCTET_LENGTH")); + } + assertEquals(pos, rs.getInt("ORDINAL_POSITION")); + assertEquals(col.nullable ? "YES" : "NO", rs.getString("IS_NULLABLE")); + assertNull(rs.getString("SCOPE_CATALOG")); + assertNull(rs.getString("SCOPE_SCHEMA")); + assertNull(rs.getString("SCOPE_TABLE")); + + assertEquals((short) 0, rs.getShort("SOURCE_DATA_TYPE")); + assertTrue(rs.wasNull()); + + assertEquals("NO", rs.getString("IS_AUTOINCREMENT")); + assertEquals(col.computed ? "YES" : "NO", rs.getString("IS_GENERATEDCOLUMN")); + assertEquals(24, rs.getMetaData().getColumnCount()); + + pos++; + } + assertFalse(rs.next()); + } + } + } + + @Test + public void testGetCrossReferences() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection + .getMetaData() + .getCrossReference( + getDefaultCatalog(), + DEFAULT_SCHEMA, + SINGERS_TABLE, + getDefaultCatalog(), + DEFAULT_SCHEMA, + ALBUMS_TABLE)) { + assertTrue(rs.next()); + + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("singers", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("albums", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("UPDATE_RULE")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); + assertEquals("PK_singers", rs.getString("PK_NAME")); + assertEquals( + (short) DatabaseMetaData.importedKeyNotDeferrable, rs.getShort("DEFERRABILITY")); + } + try (ResultSet rs = + connection + .getMetaData() + .getCrossReference( + getDefaultCatalog(), + DEFAULT_SCHEMA, + ALBUMS_TABLE, + getDefaultCatalog(), + DEFAULT_SCHEMA, + SONGS_TABLE)) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("albums", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("songs", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("UPDATE_RULE")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); + assertEquals("PK_albums", rs.getString("PK_NAME")); + assertEquals( + (short) DatabaseMetaData.importedKeyNotDeferrable, rs.getShort("DEFERRABILITY")); + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("albums", rs.getString("PKTABLE_NAME")); + assertEquals("albumid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("songs", rs.getString("FKTABLE_NAME")); + assertEquals("albumid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 2, rs.getShort("KEY_SEQ")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("UPDATE_RULE")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); + assertEquals("PK_albums", rs.getString("PK_NAME")); + assertEquals( + (short) DatabaseMetaData.importedKeyNotDeferrable, rs.getShort("DEFERRABILITY")); + + assertFalse(rs.next()); + } + + try (ResultSet rs = + connection + .getMetaData() + .getCrossReference( + getDefaultCatalog(), + DEFAULT_SCHEMA, + SINGERS_TABLE, + getDefaultCatalog(), + DEFAULT_SCHEMA, + CONCERTS_TABLE)) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("singers", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("concerts", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("UPDATE_RULE")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); + assertEquals("PK_singers", rs.getString("PK_NAME")); + assertEquals( + (short) DatabaseMetaData.importedKeyNotDeferrable, rs.getShort("DEFERRABILITY")); + + assertFalse(rs.next()); + } + + try (ResultSet rs = + connection + .getMetaData() + .getCrossReference( + getDefaultCatalog(), + DEFAULT_SCHEMA, + TABLE_WITH_ALL_COLS, + getDefaultCatalog(), + DEFAULT_SCHEMA, + TABLE_WITH_REF)) { + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("tablewithallcolumntypes", rs.getString("PKTABLE_NAME")); + assertEquals("colfloat64", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("tablewithref", rs.getString("FKTABLE_NAME")); + assertEquals("reffloat", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("UPDATE_RULE")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("DELETE_RULE")); + assertEquals("fk_tablewithref_tablewithallcolumntypes", rs.getString("FK_NAME")); + assertEquals( + (short) DatabaseMetaData.importedKeyNotDeferrable, rs.getShort("DEFERRABILITY")); + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("tablewithallcolumntypes", rs.getString("PKTABLE_NAME")); + assertEquals("colstring", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("tablewithref", rs.getString("FKTABLE_NAME")); + assertEquals("refstring", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 2, rs.getShort("KEY_SEQ")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("UPDATE_RULE")); + assertEquals((short) DatabaseMetaData.importedKeyNoAction, rs.getShort("DELETE_RULE")); + assertEquals("fk_tablewithref_tablewithallcolumntypes", rs.getString("FK_NAME")); + assertEquals( + (short) DatabaseMetaData.importedKeyNotDeferrable, rs.getShort("DEFERRABILITY")); + + assertFalse(rs.next()); + } + // try getting self-references + try (ResultSet rs = + connection + .getMetaData() + .getCrossReference( + getDefaultCatalog(), + DEFAULT_SCHEMA, + ALBUMS_TABLE, + getDefaultCatalog(), + DEFAULT_SCHEMA, + ALBUMS_TABLE)) { + assertFalse(rs.next()); + } + // try getting all cross-references in the database + try (ResultSet rs = + connection.getMetaData().getCrossReference(null, null, null, null, null, null)) { + int count = 0; + while (rs.next()) { + count++; + } + assertEquals(6, count); + } + } + } + + private static final class IndexInfo { + private final String tableName; + private final boolean nonUnique; + private final String indexName; + private final short ordinalPosition; + private final String columnName; + private final String ascDesc; + + private IndexInfo( + String tableName, + boolean nonUnique, + String indexName, + int ordinalPosition, + String columnName, + String ascDesc) { + this.tableName = tableName; + this.nonUnique = nonUnique; + this.indexName = indexName; + this.ordinalPosition = (short) ordinalPosition; + this.columnName = columnName; + this.ascDesc = ascDesc; + } + } + + private static final List EXPECTED_INDICES = + Arrays.asList( + new IndexInfo("albums", false, "PRIMARY_KEY", 1, "singerid", "A"), + new IndexInfo("albums", false, "PRIMARY_KEY", 2, "albumid", "A"), + new IndexInfo("albums", true, "albumsbyalbumtitle", 1, "albumtitle", "A"), + new IndexInfo("concerts", false, "PRIMARY_KEY", 1, "venueid", "A"), + new IndexInfo("concerts", false, "PRIMARY_KEY", 2, "singerid", "A"), + new IndexInfo("concerts", false, "PRIMARY_KEY", 3, "concertdate", "A"), + new IndexInfo("concerts", true, "GENERATED", 1, "singerid", "A"), + new IndexInfo("singers", false, "PRIMARY_KEY", 1, "singerid", "A"), + new IndexInfo("singers", true, "singersbyfirstlastname", 1, "firstname", "A"), + new IndexInfo("singers", true, "singersbyfirstlastname", 2, "lastname", "A"), + new IndexInfo("songs", false, "PRIMARY_KEY", 1, "singerid", "A"), + new IndexInfo("songs", false, "PRIMARY_KEY", 2, "albumid", "A"), + new IndexInfo("songs", false, "PRIMARY_KEY", 3, "trackid", "A"), + new IndexInfo("songs", false, "songsbysingeralbumsongnamedesc", 1, "singerid", "A"), + new IndexInfo("songs", false, "songsbysingeralbumsongnamedesc", 2, "albumid", "A"), + new IndexInfo("songs", false, "songsbysingeralbumsongnamedesc", 3, "songname", "D"), + new IndexInfo("songs", true, "songsbysongname", 1, "songname", "A"), + new IndexInfo( + "tablewithallcolumntypes", + false, + "GENERATED", + 1, + "colfloat64", + "A"), // Index automatically added by Cloud Spanner for the referencing foreign key. + new IndexInfo("tablewithallcolumntypes", false, "GENERATED", 2, "colstring", "A"), + new IndexInfo("tablewithallcolumntypes", false, "PRIMARY_KEY", 1, "colint64", "A"), + new IndexInfo("tablewithref", false, "PRIMARY_KEY", 1, "id", "A"), + new IndexInfo("tablewithref", true, "FOREIGN_KEY", 1, "reffloat", "A"), + new IndexInfo("tablewithref", true, "FOREIGN_KEY", 2, "refstring", "A")); + + @Test + public void testGetIndexInfo() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection + .getMetaData() + .getIndexInfo(getDefaultCatalog(), DEFAULT_SCHEMA, null, false, false)) { + + for (IndexInfo index : EXPECTED_INDICES) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("TABLE_SCHEM")); + assertEquals(index.tableName, rs.getString("TABLE_NAME")); + assertEquals(index.nonUnique, rs.getBoolean("NON_UNIQUE")); + assertEquals(getDefaultCatalog(), rs.getString("INDEX_QUALIFIER")); + + // Foreign key index names are automatically generated. + if (!"FOREIGN_KEY".equals(index.indexName) && !"GENERATED".equals(index.indexName)) { + assertEquals(index.indexName, rs.getString("INDEX_NAME")); + } + assertEquals(DatabaseMetaData.tableIndexHashed, rs.getShort("TYPE")); + assertEquals(index.ordinalPosition, rs.getShort("ORDINAL_POSITION")); + if (index.ordinalPosition == 0) { + assertTrue(rs.wasNull()); + } + assertEquals(index.columnName, rs.getString("COLUMN_NAME")); + assertEquals(index.ascDesc, rs.getString("ASC_OR_DESC")); + assertEquals(-1, rs.getInt("CARDINALITY")); + assertEquals(-1, rs.getInt("PAGES")); + assertNull(rs.getString("FILTER_CONDITION")); + } + // all indices found + assertFalse(rs.next()); + } + } + } + + @Test + public void testGetExportedKeys() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection + .getMetaData() + .getExportedKeys(getDefaultCatalog(), DEFAULT_SCHEMA, SINGERS_TABLE)) { + assertExportedKeysSingers(rs); + } + try (ResultSet rs = + connection + .getMetaData() + .getExportedKeys(getDefaultCatalog(), DEFAULT_SCHEMA, ALBUMS_TABLE)) { + assertKeysAlbumsSongs(rs); + } + } + } + + @Test + public void testGetImportedKeys() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection + .getMetaData() + .getImportedKeys(getDefaultCatalog(), DEFAULT_SCHEMA, SINGERS_TABLE)) { + assertImportedKeysSingers(rs); + } + try (ResultSet rs = + connection + .getMetaData() + .getImportedKeys(getDefaultCatalog(), DEFAULT_SCHEMA, ALBUMS_TABLE)) { + assertImportedKeysAlbums(rs); + } + try (ResultSet rs = + connection + .getMetaData() + .getImportedKeys(getDefaultCatalog(), DEFAULT_SCHEMA, CONCERTS_TABLE)) { + assertImportedKeysConcerts(rs); + } + try (ResultSet rs = + connection + .getMetaData() + .getImportedKeys(getDefaultCatalog(), DEFAULT_SCHEMA, SONGS_TABLE)) { + assertKeysAlbumsSongs(rs); + } + try (ResultSet rs = + connection + .getMetaData() + .getImportedKeys(getDefaultCatalog(), DEFAULT_SCHEMA, TABLE_WITH_REF)) { + assertImportedKeysTableWithRef(rs); + } + } + } + + private void assertImportedKeysSingers(ResultSet rs) throws SQLException { + assertFalse(rs.next()); + } + + private void assertImportedKeysTableWithRef(ResultSet rs) throws SQLException { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("tablewithallcolumntypes", rs.getString("PKTABLE_NAME")); + assertEquals("colfloat64", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("tablewithref", rs.getString("FKTABLE_NAME")); + assertEquals("reffloat", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertEquals("fk_tablewithref_tablewithallcolumntypes", rs.getString("FK_NAME")); + assertNotNull(rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("tablewithallcolumntypes", rs.getString("PKTABLE_NAME")); + assertEquals("colstring", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("tablewithref", rs.getString("FKTABLE_NAME")); + assertEquals("refstring", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 2, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertEquals("fk_tablewithref_tablewithallcolumntypes", rs.getString("FK_NAME")); + assertNotNull(rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertFalse(rs.next()); + } + + private void assertImportedKeysAlbums(ResultSet rs) throws SQLException { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("singers", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("albums", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); // FK name is generated. + assertEquals("PK_singers", rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertFalse(rs.next()); + } + + private void assertImportedKeysConcerts(ResultSet rs) throws SQLException { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("singers", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("concerts", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertEquals("PK_singers", rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertFalse(rs.next()); + } + + private void assertExportedKeysSingers(ResultSet rs) throws SQLException { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("singers", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("albums", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); // FK name is generated. + assertEquals("PK_singers", rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("singers", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("concerts", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); + assertEquals("PK_singers", rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertFalse(rs.next()); + } + + private void assertKeysAlbumsSongs(ResultSet rs) throws SQLException { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("albums", rs.getString("PKTABLE_NAME")); + assertEquals("singerid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("songs", rs.getString("FKTABLE_NAME")); + assertEquals("singerid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 1, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); // FK name is generated. + assertEquals("PK_albums", rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("PKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("PKTABLE_SCHEM")); + assertEquals("albums", rs.getString("PKTABLE_NAME")); + assertEquals("albumid", rs.getString("PKCOLUMN_NAME")); + assertEquals(getDefaultCatalog(), rs.getString("FKTABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("FKTABLE_SCHEM")); + assertEquals("songs", rs.getString("FKTABLE_NAME")); + assertEquals("albumid", rs.getString("FKCOLUMN_NAME")); + assertEquals((short) 2, rs.getShort("KEY_SEQ")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyRestrict, rs.getInt("DELETE_RULE")); + assertNotNull(rs.getString("FK_NAME")); // FK name is generated. + assertEquals("PK_albums", rs.getString("PK_NAME")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, rs.getInt("DEFERRABILITY")); + + assertFalse(rs.next()); + } + + @Test + public void testGetPrimaryKeys() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection + .getMetaData() + .getPrimaryKeys(getDefaultCatalog(), DEFAULT_SCHEMA, SINGERS_TABLE)) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("TABLE_SCHEM")); + assertEquals("singers", rs.getString("TABLE_NAME")); + assertEquals("singerid", rs.getString("COLUMN_NAME")); + assertEquals(1, rs.getInt("KEY_SEQ")); + assertEquals("PRIMARY_KEY", rs.getString("PK_NAME")); + assertFalse(rs.next()); + } + + try (ResultSet rs = + connection + .getMetaData() + .getPrimaryKeys(getDefaultCatalog(), DEFAULT_SCHEMA, ALBUMS_TABLE)) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("TABLE_SCHEM")); + assertEquals("albums", rs.getString("TABLE_NAME")); + assertEquals("singerid", rs.getString("COLUMN_NAME")); + assertEquals(1, rs.getInt("KEY_SEQ")); + assertEquals("PRIMARY_KEY", rs.getString("PK_NAME")); + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("TABLE_SCHEM")); + assertEquals("albums", rs.getString("TABLE_NAME")); + assertEquals("albumid", rs.getString("COLUMN_NAME")); + assertEquals(2, rs.getInt("KEY_SEQ")); + assertEquals("PRIMARY_KEY", rs.getString("PK_NAME")); + + assertFalse(rs.next()); + } + } + } + + @Ignore("Views are not yet supported for PostgreSQL") + @Test + public void testGetViews() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection + .getMetaData() + .getTables(getDefaultCatalog(), DEFAULT_SCHEMA, null, new String[] {"VIEW"})) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("TABLE_SCHEM")); + assertEquals("SingersView", rs.getString("TABLE_NAME")); + assertFalse(rs.next()); + } + } + } + + @Test + public void testGetSchemas() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = connection.getMetaData().getSchemas()) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CATALOG")); + assertEquals("information_schema", rs.getString("TABLE_SCHEM")); + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CATALOG")); + assertEquals("public", rs.getString("TABLE_SCHEM")); + + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CATALOG")); + assertEquals("spanner_sys", rs.getString("TABLE_SCHEM")); + + assertFalse(rs.next()); + } + } + } + + private static final class Table { + private final String name; + private final String type; + + private Table(String name) { + this(name, "TABLE"); + } + + private Table(String name, String type) { + this.name = name; + this.type = type; + } + } + + private static final List EXPECTED_TABLES = + Arrays.asList( + new Table("albums"), + new Table("concerts"), + new Table("singers"), + // TODO: Enable when views are supported for PostgreSQL dialect databases. + // new Table("singersview", "VIEW"), + new Table("songs"), + new Table("tablewithallcolumntypes"), + new Table("tablewithref")); + + @Test + public void testGetTables() throws SQLException { + try (Connection connection = createConnection()) { + try (ResultSet rs = + connection.getMetaData().getTables(getDefaultCatalog(), DEFAULT_SCHEMA, null, null)) { + for (Table table : EXPECTED_TABLES) { + assertTrue(rs.next()); + assertEquals(getDefaultCatalog(), rs.getString("TABLE_CAT")); + assertEquals(DEFAULT_SCHEMA, rs.getString("TABLE_SCHEM")); + assertEquals(table.name, rs.getString("TABLE_NAME")); + assertEquals(table.type, rs.getString("TABLE_TYPE")); + assertNull(rs.getString("REMARKS")); + assertNull(rs.getString("TYPE_CAT")); + assertNull(rs.getString("TYPE_SCHEM")); + assertNull(rs.getString("TYPE_NAME")); + assertNull(rs.getString("SELF_REFERENCING_COL_NAME")); + assertNull(rs.getString("REF_GENERATION")); + } + assertFalse(rs.next()); + } + } + } +} diff --git a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql index ca6adcb0..55054231 100644 --- a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql +++ b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql @@ -17,35 +17,35 @@ START BATCH DDL; CREATE TABLE Singers ( - SingerId BIGINT PRIMARY KEY, - FirstName VARCHAR(1024), - LastName VARCHAR(1024), - SingerInfo BYTEA, - BirthDate VARCHAR + SingerId BIGINT PRIMARY KEY, + FirstName VARCHAR(1024), + LastName VARCHAR(1024), + SingerInfo BYTEA, + BirthDate VARCHAR ); CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName); CREATE TABLE Albums ( - SingerId BIGINT NOT NULL, - AlbumId BIGINT NOT NULL, - AlbumTitle VARCHAR, - MarketingBudget BIGINT, - PRIMARY KEY(SingerId, AlbumId), - FOREIGN KEY(SingerId) REFERENCES Singers(SingerID) + SingerId BIGINT NOT NULL, + AlbumId BIGINT NOT NULL, + AlbumTitle VARCHAR, + MarketingBudget BIGINT, + PRIMARY KEY(SingerId, AlbumId), + FOREIGN KEY(SingerId) REFERENCES Singers(SingerID) ); CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle); CREATE TABLE Songs ( - SingerId BIGINT NOT NULL, - AlbumId BIGINT NOT NULL, - TrackId BIGINT NOT NULL, - SongName VARCHAR, - Duration BIGINT, - SongGenre VARCHAR(25), - PRIMARY KEY(SingerId, AlbumId, TrackId), - FOREIGN KEY(AlbumId) REFERENCES Albums(AlbumId) + SingerId BIGINT NOT NULL, + AlbumId BIGINT NOT NULL, + TrackId BIGINT NOT NULL, + SongName VARCHAR, + Duration BIGINT, + SongGenre VARCHAR(25), + PRIMARY KEY(SingerId, AlbumId, TrackId), + FOREIGN KEY(SingerId, AlbumId) REFERENCES Albums(SingerId, AlbumId) ); CREATE UNIQUE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC); @@ -53,21 +53,33 @@ CREATE UNIQUE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, S CREATE INDEX SongsBySongName ON Songs(SongName); CREATE TABLE Concerts ( - VenueId BIGINT NOT NULL, - SingerId BIGINT NOT NULL, - ConcertDate VARCHAR NOT NULL, - BeginTime VARCHAR, - EndTime VARCHAR, - PRIMARY KEY(VenueId, SingerId, ConcertDate) + VenueId BIGINT NOT NULL, + SingerId BIGINT NOT NULL, + ConcertDate VARCHAR NOT NULL, + BeginTime VARCHAR, + EndTime VARCHAR, + PRIMARY KEY(VenueId, SingerId, ConcertDate), + FOREIGN KEY(SingerId) REFERENCES Singers(SingerId) ); CREATE TABLE TableWithAllColumnTypes ( - ColInt64 BIGINT PRIMARY KEY, - ColFloat8 FLOAT8 NOT NULL, - ColBool BOOL NOT NULL, - ColString VARCHAR(100) NOT NULL, - ColBytes BYTEA NOT NULL, - ColNumeric NUMERIC NOT NULL + ColInt64 BIGINT PRIMARY KEY, + ColFloat64 FLOAT8 NOT NULL, + ColBool BOOL NOT NULL, + ColString VARCHAR(100) NOT NULL, + ColStringMax TEXT, + ColBytes BYTEA NOT NULL, + ColTimestamp TIMESTAMP WITH TIME ZONE, + ColNumeric NUMERIC NOT NULL +); + +CREATE TABLE TableWithRef ( + Id BIGINT NOT NULL PRIMARY KEY, + RefFloat FLOAT8 NOT NULL, + RefString VARCHAR(100) NOT NULL, + CONSTRAINT Fk_TableWithRef_TableWithAllColumnTypes + FOREIGN KEY (RefFloat, RefString) + REFERENCES TableWithAllColumnTypes (ColFloat64, ColString) ); RUN BATCH;