Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions presto-blackhole/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@
<artifactId>guava</artifactId>
</dependency>

<dependency>
<groupId>com.facebook.airlift</groupId>
<artifactId>configuration</artifactId>
</dependency>

<dependency>
<groupId>com.facebook.airlift</groupId>
<artifactId>json</artifactId>
</dependency>

<dependency>
<groupId>com.facebook.airlift</groupId>
<artifactId>bootstrap</artifactId>
</dependency>

<dependency>
<groupId>com.facebook.airlift</groupId>
<artifactId>concurrent</artifactId>
Expand Down Expand Up @@ -92,9 +107,8 @@
</dependency>

<dependency>
<groupId>com.facebook.airlift</groupId>
<artifactId>log-manager</artifactId>
<scope>test</scope>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.facebook.presto.plugin.blackhole;

import com.facebook.airlift.configuration.Config;
import com.facebook.airlift.configuration.ConfigDescription;

public class BlackHoleClientConfig
{
private boolean caseSensitiveNameMatchingEnabled;

public boolean isCaseSensitiveNameMatching()
{
return caseSensitiveNameMatchingEnabled;
}

@Config("case-sensitive-name-matching")
@ConfigDescription("Enable case-sensitive matching of schema, table names across the connector. " +
"When disabled, names are matched case-insensitively using lowercase normalization.")
public BlackHoleClientConfig setCaseSensitiveNameMatching(boolean caseSensitiveNameMatchingEnabled)
{
this.caseSensitiveNameMatchingEnabled = caseSensitiveNameMatchingEnabled;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class BlackHoleConnector
private final BlackHoleNodePartitioningProvider partitioningProvider;
private final TypeManager typeManager;
private final ExecutorService executorService;
private final boolean caseSensitiveNameMatching;

public BlackHoleConnector(
BlackHoleMetadata metadata,
Expand All @@ -63,7 +64,8 @@ public BlackHoleConnector(
BlackHolePageSinkProvider pageSinkProvider,
BlackHoleNodePartitioningProvider partitioningProvider,
TypeManager typeManager,
ExecutorService executorService)
ExecutorService executorService,
BlackHoleClientConfig config)
{
this.metadata = metadata;
this.splitManager = splitManager;
Expand All @@ -72,6 +74,12 @@ public BlackHoleConnector(
this.partitioningProvider = partitioningProvider;
this.typeManager = typeManager;
this.executorService = executorService;
this.caseSensitiveNameMatching = config.isCaseSensitiveNameMatching();
}

private String normalizeColumnName(String name)
{
return caseSensitiveNameMatching ? name : name.toLowerCase(ENGLISH);
}

@Override
Expand Down Expand Up @@ -143,7 +151,7 @@ public List<PropertyMetadata<?>> getTableProperties()
ImmutableList.of(),
false,
value -> ImmutableList.copyOf(((List<String>) value).stream()
.map(name -> name.toLowerCase(ENGLISH))
.map(this::normalizeColumnName)
.collect(toList())),
List.class::cast),
new PropertyMetadata<>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
*/
package com.facebook.presto.plugin.blackhole;

import com.facebook.airlift.bootstrap.Bootstrap;
import com.facebook.airlift.json.JsonModule;
import com.facebook.presto.spi.ConnectorHandleResolver;
import com.facebook.presto.spi.connector.Connector;
import com.facebook.presto.spi.connector.ConnectorContext;
import com.facebook.presto.spi.connector.ConnectorFactory;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.inject.Injector;

import java.util.Map;

import static com.facebook.airlift.concurrent.Threads.daemonThreadsNamed;
import static com.facebook.airlift.configuration.ConfigBinder.configBinder;
import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;

Expand All @@ -43,14 +47,33 @@ public ConnectorHandleResolver getHandleResolver()
@Override
public Connector create(String catalogName, Map<String, String> requiredConfig, ConnectorContext context)
{
ListeningScheduledExecutorService executorService = listeningDecorator(newSingleThreadScheduledExecutor(daemonThreadsNamed("blackhole")));
return new BlackHoleConnector(
new BlackHoleMetadata(),
new BlackHoleSplitManager(),
new BlackHolePageSourceProvider(executorService),
new BlackHolePageSinkProvider(executorService),
new BlackHoleNodePartitioningProvider(context.getNodeManager()),
context.getTypeManager(),
executorService);
try {
Bootstrap app = new Bootstrap(
new JsonModule(),
binder -> {
configBinder(binder).bindConfig(BlackHoleClientConfig.class);
});

Injector injector = app
.doNotInitializeLogging()
.setRequiredConfigurationProperties(requiredConfig)
.initialize();

BlackHoleClientConfig config = injector.getInstance(BlackHoleClientConfig.class);

ListeningScheduledExecutorService executorService = listeningDecorator(newSingleThreadScheduledExecutor(daemonThreadsNamed("blackhole")));
return new BlackHoleConnector(
new BlackHoleMetadata(),
new BlackHoleSplitManager(),
new BlackHolePageSourceProvider(executorService),
new BlackHolePageSinkProvider(executorService),
new BlackHoleNodePartitioningProvider(context.getNodeManager()),
context.getTypeManager(),
executorService,
config);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
import static com.facebook.presto.spi.StandardErrorCode.INVALID_TABLE_PROPERTY;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.lang.String.format;
import static java.util.Locale.ENGLISH;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
Expand All @@ -67,10 +69,23 @@ public class BlackHoleMetadata

private final List<String> schemas = new ArrayList<>();
private final Map<SchemaTableName, BlackHoleTableHandle> tables = new ConcurrentHashMap<>();
private boolean caseSensitiveNameMatchingEnabled;

public BlackHoleMetadata()
{
this(new BlackHoleClientConfig());
}

public BlackHoleMetadata(BlackHoleClientConfig config)
{
schemas.add(SCHEMA_NAME);
this.caseSensitiveNameMatchingEnabled = requireNonNull(config, "config is null").isCaseSensitiveNameMatching();
}

@Override
public String normalizeIdentifier(ConnectorSession session, String identifier)
{
return caseSensitiveNameMatchingEnabled ? identifier : identifier.toLowerCase(ENGLISH);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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.facebook.presto.plugin.blackhole;

import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.testing.TestingConnectorSession;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

public class TestBlackHoleCaseSensitivity
{
private static final ConnectorSession SESSION = TestingConnectorSession.SESSION;

@Test
public void testBlackHoleClientConfigDefaults()
{
BlackHoleClientConfig config = new BlackHoleClientConfig();
assertFalse(config.isCaseSensitiveNameMatching(),
"Default should be case-insensitive");
}

@Test
public void testBlackHoleClientConfigCaseSensitive()
{
BlackHoleClientConfig config = new BlackHoleClientConfig()
.setCaseSensitiveNameMatching(true);

assertTrue(config.isCaseSensitiveNameMatching(),
"Should be case-sensitive when enabled");
}

@Test
public void testBlackHoleClientConfigCaseInsensitive()
{
BlackHoleClientConfig config = new BlackHoleClientConfig()
.setCaseSensitiveNameMatching(false);

assertFalse(config.isCaseSensitiveNameMatching(),
"Should be case-insensitive when disabled");
}

@Test
public void testBlackHoleMetadataNormalizeIdentifierCaseSensitive()
{
BlackHoleClientConfig config = new BlackHoleClientConfig()
.setCaseSensitiveNameMatching(true);
BlackHoleMetadata metadata = new BlackHoleMetadata(config);

// Case-sensitive: identifiers should remain unchanged
assertEquals(metadata.normalizeIdentifier(SESSION, "MyTable"), "MyTable");
assertEquals(metadata.normalizeIdentifier(SESSION, "myTable"), "myTable");
assertEquals(metadata.normalizeIdentifier(SESSION, "MYTABLE"), "MYTABLE");
assertEquals(metadata.normalizeIdentifier(SESSION, "MySchema"), "MySchema");
assertEquals(metadata.normalizeIdentifier(SESSION, "Test_Table"), "Test_Table");
}

@Test
public void testBlackHoleMetadataNormalizeIdentifierCaseInsensitive()
{
BlackHoleClientConfig config = new BlackHoleClientConfig()
.setCaseSensitiveNameMatching(false);
BlackHoleMetadata metadata = new BlackHoleMetadata(config);

// Case-insensitive: identifiers should be lowercased
assertEquals(metadata.normalizeIdentifier(SESSION, "MyTable"), "mytable");
assertEquals(metadata.normalizeIdentifier(SESSION, "myTable"), "mytable");
assertEquals(metadata.normalizeIdentifier(SESSION, "MYTABLE"), "mytable");
assertEquals(metadata.normalizeIdentifier(SESSION, "MySchema"), "myschema");
assertEquals(metadata.normalizeIdentifier(SESSION, "Test_Table"), "test_table");
}

@Test
public void testBlackHoleMetadataDefaultBehavior()
{
// Default metadata should be case-insensitive
BlackHoleMetadata metadata = new BlackHoleMetadata();

assertEquals(metadata.normalizeIdentifier(SESSION, "MyTable"), "mytable");
assertEquals(metadata.normalizeIdentifier(SESSION, "TestSchema"), "testschema");
assertEquals(metadata.normalizeIdentifier(SESSION, "UPPERCASE"), "uppercase");
}

@Test
public void testConfigIntegrationWithMetadata()
{
// Test that config changes are properly reflected in metadata behavior

// Case-sensitive config
BlackHoleClientConfig caseSensitiveConfig = new BlackHoleClientConfig()
.setCaseSensitiveNameMatching(true);
BlackHoleMetadata caseSensitiveMetadata = new BlackHoleMetadata(caseSensitiveConfig);

// Case-insensitive config
BlackHoleClientConfig caseInsensitiveConfig = new BlackHoleClientConfig()
.setCaseSensitiveNameMatching(false);
BlackHoleMetadata caseInsensitiveMetadata = new BlackHoleMetadata(caseInsensitiveConfig);

String testIdentifier = "MyTestTable";

assertEquals(caseSensitiveMetadata.normalizeIdentifier(SESSION, testIdentifier), "MyTestTable");
assertEquals(caseInsensitiveMetadata.normalizeIdentifier(SESSION, testIdentifier), "mytesttable");
}

@Test
public void testMultipleConfigInstances()
{
// Test that multiple config instances work independently
BlackHoleClientConfig config1 = new BlackHoleClientConfig().setCaseSensitiveNameMatching(true);
BlackHoleClientConfig config2 = new BlackHoleClientConfig().setCaseSensitiveNameMatching(false);

assertTrue(config1.isCaseSensitiveNameMatching());
assertFalse(config2.isCaseSensitiveNameMatching());

// Test that they don't interfere with each other
BlackHoleMetadata metadata1 = new BlackHoleMetadata(config1);
BlackHoleMetadata metadata2 = new BlackHoleMetadata(config2);

assertEquals(metadata1.normalizeIdentifier(SESSION, "TestCase"), "TestCase");
assertEquals(metadata2.normalizeIdentifier(SESSION, "TestCase"), "testcase");
}

@Test
public void testConfigChaining()
{
// Test that config setter returns the config instance for chaining
BlackHoleClientConfig config = new BlackHoleClientConfig()
.setCaseSensitiveNameMatching(true)
.setCaseSensitiveNameMatching(false)
.setCaseSensitiveNameMatching(true);

assertTrue(config.isCaseSensitiveNameMatching(),
"Final value should be true after chaining");
}
}
Loading