Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support default schema and catalog for PostgreSQL databases #1375

Merged
merged 5 commits into from Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 43 additions & 7 deletions src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
Expand Up @@ -47,6 +47,7 @@
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import javax.annotation.Nonnull;

/** Jdbc Connection class for Google Cloud Spanner */
class JdbcConnection extends AbstractJdbcConnection {
Expand Down Expand Up @@ -394,31 +395,66 @@ public Array createArrayOf(String typeName, Object[] elements) throws SQLExcepti
@Override
public void setCatalog(String catalog) throws SQLException {
// This method could be changed to allow the user to change to another database.
// For now we only support setting an empty string in order to support frameworks
// For now, we only support setting the default catalog in order to support frameworks
// and applications that set this when no catalog has been specified in the connection
// URL.
checkClosed();
JdbcPreconditions.checkArgument("".equals(catalog), "Only catalog \"\" is supported");
checkValidCatalog(catalog);
}

void checkValidCatalog(String catalog) throws SQLException {
String defaultCatalog = getDefaultCatalog();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the variable here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use it twice on the next line... So we don't need it, but otherwise we would have to call the method twice on the next line, which also does not feel great.

JdbcPreconditions.checkArgument(
defaultCatalog.equals(catalog),
String.format("Only catalog %s is supported", defaultCatalog));
}

@Override
public String getCatalog() throws SQLException {
checkClosed();
return "";
return getDefaultCatalog();
}

@Nonnull
String getDefaultCatalog() {
switch (getDialect()) {
case POSTGRESQL:
String database = getConnectionOptions().getDatabaseName();
// It should not be possible that database is null, but it's better to be safe than sorry.
return database == null ? "" : database;
case GOOGLE_STANDARD_SQL:
default:
return "";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are returning empty string at multiple places, would it be better to use a constant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say no for a couple of reasons:

  1. The JVM is internally creating a constant for this anyways, so there's no performance gain to be gotten.
  2. Using a constant means that you have to navigate to that constant to see what the value actually is (although good naming would help clarify that a lot in this case)
  3. Someone might be tempted to use the constant in tests as well to verify the value that is being returned. That again means that if someone changes the constant value, the test will still succeed. But real code that depends on this behavior might change.

}
}

@Override
public void setSchema(String schema) throws SQLException {
checkClosed();
// Cloud Spanner does not support schemas, but does contain a pseudo 'empty string' schema that
// might be set by frameworks and applications that read the database metadata.
JdbcPreconditions.checkArgument("".equals(schema), "Only schema \"\" is supported");
checkValidSchema(schema);
}

void checkValidSchema(String schema) throws SQLException {
String defaultSchema = getDefaultSchema();
JdbcPreconditions.checkArgument(
defaultSchema.equals(schema), String.format("Only schema %s is supported", defaultSchema));
}

@Override
public String getSchema() throws SQLException {
checkClosed();
return "";
return getDefaultSchema();
}

@Nonnull
String getDefaultSchema() {
switch (getDialect()) {
case POSTGRESQL:
return "public";
case GOOGLE_STANDARD_SQL:
default:
return "";
}
}

@Override
Expand Down
Expand Up @@ -777,11 +777,12 @@ public ResultSet getSchemas() throws SQLException {
}

@Override
public ResultSet getCatalogs() {
public ResultSet getCatalogs() throws SQLException {
return JdbcResultSet.of(
ResultSets.forRows(
Type.struct(StructField.of("TABLE_CAT", Type.string())),
Collections.singletonList(Struct.newBuilder().set("TABLE_CAT").to("").build())));
Collections.singletonList(
Struct.newBuilder().set("TABLE_CAT").to(getConnection().getCatalog()).build())));
}

@Override
Expand Down Expand Up @@ -1524,9 +1525,10 @@ public RowIdLifetime getRowIdLifetime() {
@Override
public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException {
String sql = readSqlFromFile("DatabaseMetaData_GetSchemas.sql", connection.getDialect());
JdbcPreparedStatement statement =
prepareStatementReplaceNullWithAnyString(sql, catalog, schemaPattern);
return statement.executeQueryWithOptions(InternalMetadataQuery.INSTANCE);
try (JdbcPreparedStatement statement =
prepareStatementReplaceNullWithAnyString(sql, catalog, schemaPattern)) {
return statement.executeQueryWithOptions(InternalMetadataQuery.INSTANCE);
}
}

@Override
Expand Down
Expand Up @@ -20,6 +20,7 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
Expand Down Expand Up @@ -700,34 +701,33 @@ public void testCatalog() throws SQLException {
ConnectionOptions options = mockOptions();
when(options.getDatabaseName()).thenReturn("test");
try (JdbcConnection connection = createConnection(options)) {
// The connection should always return the empty string as the current catalog, as no other
// The connection should always return the default catalog as the current catalog, as no other
// catalogs exist in the INFORMATION_SCHEMA.
assertThat(connection.getCatalog()).isEqualTo("");
// The default catalog is the empty string for GoogleSQL databases.
// The default catalog is the database name for PostgreSQL databases.
assertEquals(connection.getDefaultCatalog(), connection.getCatalog());
// This should be allowed.
connection.setCatalog("");
try {
// This should cause an exception.
connection.setCatalog("other");
fail("missing expected exception");
} catch (JdbcSqlExceptionImpl e) {
assertThat(e.getCode()).isEqualTo(Code.INVALID_ARGUMENT);
}
connection.setCatalog(connection.getDefaultCatalog());
// This should cause an exception.
JdbcSqlExceptionImpl exception =
assertThrows(JdbcSqlExceptionImpl.class, () -> connection.setCatalog("other"));
assertEquals(Code.INVALID_ARGUMENT, exception.getCode());
}
}

@Test
public void testSchema() throws SQLException {
try (JdbcConnection connection = createConnection(mockOptions())) {
assertThat(connection.getSchema()).isEqualTo("");
// The connection should always return the default schema as the current schema, as we
// currently do not support setting the connection to a different schema.
// The default schema is the empty string for GoogleSQL databases.
// The default schema is 'public' for PostgreSQL databases.
assertEquals(connection.getDefaultSchema(), connection.getSchema());
// This should be allowed.
connection.setSchema("");
try {
// This should cause an exception.
connection.setSchema("other");
fail("missing expected exception");
} catch (JdbcSqlExceptionImpl e) {
assertThat(e.getCode()).isEqualTo(Code.INVALID_ARGUMENT);
}
connection.setSchema(connection.getDefaultSchema());
JdbcSqlExceptionImpl exception =
assertThrows(JdbcSqlExceptionImpl.class, () -> connection.setSchema("other"));
assertEquals(Code.INVALID_ARGUMENT, exception.getCode());
}
}

Expand Down
Expand Up @@ -41,10 +41,19 @@
import java.util.Objects;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

@RunWith(JUnit4.class)
@RunWith(Parameterized.class)
public class JdbcDatabaseMetaDataTest {
@Parameter public Dialect dialect;

@Parameters(name = "dialect = {0}")
public static Object[] data() {
return Dialect.values();
}

private static final String DEFAULT_CATALOG = "";
private static final String DEFAULT_SCHEMA = "";
private static final String TEST_TABLE = "FOO";
Expand Down Expand Up @@ -314,10 +323,12 @@ public void testGetBestRowIdentifier() throws SQLException {
@Test
public void testGetCatalogs() throws SQLException {
JdbcConnection connection = mock(JdbcConnection.class);
when(connection.getDialect()).thenReturn(dialect);
when(connection.getCatalog()).thenCallRealMethod();
DatabaseMetaData meta = new JdbcDatabaseMetaData(connection);
try (ResultSet rs = meta.getCatalogs()) {
assertThat(rs.next(), is(true));
assertThat(rs.getString("TABLE_CAT"), is(equalTo("")));
assertThat(rs.getString("TABLE_CAT"), is(equalTo(connection.getDefaultCatalog())));
assertThat(rs.next(), is(false));
ResultSetMetaData rsmd = rs.getMetaData();
assertThat(rsmd.getColumnCount(), is(equalTo(1)));
Expand Down
Expand Up @@ -822,18 +822,30 @@ public void testGetViews() throws SQLException {
@Test
public void testGetSchemas() throws SQLException {
try (Connection connection = createConnection(env, database)) {
assertEquals("", connection.getSchema());
try (ResultSet rs = connection.getMetaData().getSchemas()) {
assertThat(rs.next(), is(true));
assertThat(rs.getString("TABLE_SCHEM"), is(equalTo(DEFAULT_SCHEMA)));
assertThat(rs.getString("TABLE_CATALOG"), is(equalTo(DEFAULT_CATALOG)));
assertThat(rs.next(), is(true));
assertThat(rs.getString("TABLE_SCHEM"), is(equalTo("INFORMATION_SCHEMA")));
assertThat(rs.getString("TABLE_CATALOG"), is(equalTo(DEFAULT_CATALOG)));
if (!EmulatorSpannerHelper.isUsingEmulator()) {
assertThat(rs.next(), is(true));
assertThat(rs.getString("TABLE_SCHEM"), is(equalTo("SPANNER_SYS")));
assertThat(rs.getString("TABLE_CATALOG"), is(equalTo(DEFAULT_CATALOG)));
}
assertThat(rs.next(), is(true));
assertThat(rs.getString("TABLE_SCHEM"), is(equalTo("SPANNER_SYS")));
assertThat(rs.getString("TABLE_CATALOG"), is(equalTo(DEFAULT_CATALOG)));
assertFalse(rs.next());
}
}
}

@Test
public void testGetCatalogs() throws SQLException {
try (Connection connection = createConnection(env, database)) {
assertEquals("", connection.getCatalog());
try (ResultSet rs = connection.getMetaData().getCatalogs()) {
assertTrue(rs.next());
assertEquals("", rs.getString("TABLE_CAT"));

assertFalse(rs.next());
}
}
Expand Down
Expand Up @@ -752,6 +752,7 @@ public void testGetViews() throws SQLException {
@Test
public void testGetSchemas() throws SQLException {
try (Connection connection = createConnection(env, database)) {
assertEquals("public", connection.getSchema());
try (ResultSet rs = connection.getMetaData().getSchemas()) {
assertTrue(rs.next());
assertEquals(getDefaultCatalog(database), rs.getString("TABLE_CATALOG"));
Expand All @@ -774,6 +775,19 @@ public void testGetSchemas() throws SQLException {
}
}

@Test
public void testGetCatalogs() throws SQLException {
try (Connection connection = createConnection(env, database)) {
assertEquals(database.getId().getDatabase(), connection.getCatalog());
try (ResultSet rs = connection.getMetaData().getCatalogs()) {
assertTrue(rs.next());
assertEquals(database.getId().getDatabase(), rs.getString("TABLE_CAT"));

assertFalse(rs.next());
}
}
}

private static final class Table {
private final String name;
private final String type;
Expand Down