Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9fa9644
A schema provider to get metadata through Jdbc
OpenOpened Jan 9, 2020
1926c44
addition maven compile dependency
OpenOpened Jan 9, 2020
b821f63
Optimize code and provide test cases
OpenOpened Jan 13, 2020
f78a11b
Merge branch 'master' into jdbc-provider
OpenOpened Jan 13, 2020
368fa3c
Merge branch 'github-master' into jdbc-provider
OpenOpened Jan 14, 2020
fa75537
Merge remote-tracking branch 'origin/jdbc-provider' into jdbc-provider
OpenOpened Jan 14, 2020
bf6bbd9
update code to fit spark 2.4.4 version
OpenOpened Jan 14, 2020
2a69e3a
Merge branch 'github-master' into jdbc-provider
OpenOpened Jan 14, 2020
0a4cffe
fix bug
OpenOpened Jan 14, 2020
95d404a
Optimize code
OpenOpened Jan 14, 2020
83c671d
Merge branch 'github-master' into jdbc-provider
OpenOpened Jan 14, 2020
de0566a
auto close jsc object
OpenOpened Jan 14, 2020
ad18cae
Adding license to TestJdbcbasedSchemaProvider class
vinothchandar Jan 26, 2020
f53e725
Merge branch 'github-master' into jdbc-provider
OpenOpened Feb 8, 2020
13e80e2
Merge remote-tracking branch 'origin/jdbc-provider' into jdbc-provider
OpenOpened Feb 8, 2020
d183851
fix bug and resolve conflict
OpenOpened Feb 8, 2020
57061ed
try-with-resources code optimize
OpenOpened Feb 8, 2020
013babf
fix code style problem
OpenOpened Feb 8, 2020
450dde9
fix variable name
OpenOpened Feb 8, 2020
f7914c1
addition a little comments
OpenOpened Feb 10, 2020
a44a52a
remove extra comments
OpenOpened Feb 10, 2020
ca14ac9
reimplement code using java
OpenOpened Feb 12, 2020
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
7 changes: 7 additions & 0 deletions hudi-utilities/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@
</build>

<dependencies>
<!-- H2 database for JdbcbaseSchemaProvider -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
<scope>test</scope>
</dependency>

<!-- Jetty -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

package org.apache.hudi.utilities;

import org.apache.avro.Schema;
import org.apache.hudi.AvroConversionUtils;
import org.apache.hudi.HoodieWriteClient;
import org.apache.hudi.WriteStatus;
import org.apache.hudi.common.util.DFSPropertiesConfiguration;
Expand All @@ -27,6 +29,7 @@
import org.apache.hudi.config.HoodieCompactionConfig;
import org.apache.hudi.config.HoodieIndexConfig;
import org.apache.hudi.config.HoodieWriteConfig;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.exception.HoodieIOException;
import org.apache.hudi.index.HoodieIndex;
import org.apache.hudi.utilities.schema.SchemaProvider;
Expand All @@ -45,12 +48,22 @@
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.launcher.SparkLauncher;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.execution.datasources.jdbc.JDBCOptions;
import org.apache.spark.sql.execution.datasources.jdbc.JdbcOptionsInWrite;
import org.apache.spark.sql.execution.datasources.jdbc.JdbcUtils;
import org.apache.spark.sql.jdbc.JdbcDialect;
import org.apache.spark.sql.jdbc.JdbcDialects;
import org.apache.spark.sql.types.StructType;
import scala.Tuple2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -235,4 +248,57 @@ public static TypedProperties readConfig(InputStream in) throws IOException {
defaults.load(in);
return defaults;
}

/***
* call spark function get the schema through jdbc.
* The code logic implementation refers to spark 2.4.x and spark 3.x.
* @param options
* @return
* @throws Exception
*/
public static Schema getJDBCSchema(Map<String, String> options) throws Exception {
scala.collection.immutable.Map<String, String> ioptions = toScalaImmutableMap(options);
JDBCOptions jdbcOptions = new JDBCOptions(ioptions);
Connection conn = JdbcUtils.createConnectionFactory(jdbcOptions).apply();
String url = jdbcOptions.url();
String table = jdbcOptions.tableOrQuery();
JdbcOptionsInWrite jdbcOptionsInWrite = new JdbcOptionsInWrite(ioptions);
boolean tableExists = JdbcUtils.tableExists(conn, jdbcOptionsInWrite);

if (tableExists) {
JdbcDialect dialect = JdbcDialects.get(url);
try (PreparedStatement statement = conn.prepareStatement(dialect.getSchemaQuery(table))) {
statement.setQueryTimeout(Integer.parseInt(options.get("timeout")));
try (ResultSet rs = statement.executeQuery()) {
StructType structType;
if (Boolean.parseBoolean(ioptions.get("nullable").get())) {
structType = JdbcUtils.getSchema(rs, dialect, true);
} else {
structType = JdbcUtils.getSchema(rs, dialect, false);
}
return AvroConversionUtils.convertStructTypeToAvroSchema(structType, table, "hoodie." + table);
}
}
} else {
throw new HoodieException(String.format("%s table does not exists!", table));
}
}

/**
* Replace java map with scala immutable map.
* refers: https://stackoverflow.com/questions/11903167/convert-java-util-hashmap-to-scala-collection-immutable-map-in-java/11903737#11903737

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we could remove this line, If we copy from other projects, we may need add copyright to LICENSE. but if copied from stackoverflow, it would be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok, i will remove it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry to be a pain. but this piece of code would be problematic since its exactly https://stackoverflow.com/a/45992769 ? Please re-implement this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you guys have a better way? I checked the relevant information. If you want to achieve conversion elegantly, there are only two methods. First, using scala code to do the conversion, you need to implement a scala conversion class. Second, rewrite the related spark method with java. The disadvantage is that it may cause compatibility problems in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@vinothchandar @leesf I reimplemented the relevant logic using the second approach.

* @param javaMap
* @param <K>
* @param <V>
* @return
*/
@SuppressWarnings("unchecked")
private static <K, V> scala.collection.immutable.Map<K, V> toScalaImmutableMap(java.util.Map<K, V> javaMap) {
Comment thread
vinothchandar marked this conversation as resolved.
Outdated
final java.util.List<scala.Tuple2<K, V>> list = new java.util.ArrayList<>(javaMap.size());
for (final java.util.Map.Entry<K, V> entry : javaMap.entrySet()) {
list.add(scala.Tuple2.apply(entry.getKey(), entry.getValue()));
}
final scala.collection.Seq<Tuple2<K, V>> seq = scala.collection.JavaConverters.asScalaBufferConverter(list).asScala().toSeq();
return (scala.collection.immutable.Map<K, V>) scala.collection.immutable.Map$.MODULE$.apply(seq);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.hudi.utilities.schema;

import org.apache.avro.Schema;
import org.apache.hudi.common.util.TypedProperties;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.utilities.UtilHelpers;
import org.apache.spark.api.java.JavaSparkContext;

import java.util.HashMap;
import java.util.Map;

/**
* A schema provider to get metadata through Jdbc.
*/
public class JdbcbasedSchemaProvider extends SchemaProvider {
private Schema sourceSchema;
private Map<String, String> options = new HashMap<>();

/**
* Configs supported.
*/
public static class Config {
// The JDBC URL to connect to. The source-specific connection properties may be specified in the URL.
// e.g., jdbc:postgresql://localhost/test?user=fred&password=secret
private static final String SOURCE_SCHEMA_JDBC_CONNECTION_URL = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.connection.url";
// The class name of the JDBC driver to use to connect to this URL. such as org.h2.Driver
private static final String SOURCE_SCHEMA_JDBC_DRIVER_TYPE = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.driver.type";
private static final String SOURCE_SCHEMA_JDBC_USERNAME = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.username";
private static final String SOURCE_SCHEMA_JDBC_PASSWORD = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.password";
// example : test_database.test1_table or test1_table
private static final String SOURCE_SCHEMA_JDBC_DBTABLE = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.dbtable";
// The number of seconds the driver will wait for a Statement object to execute to the given number of seconds.
// Zero means there is no limit. In the write path, this option depends on how JDBC drivers implement the API setQueryTimeout,
// e.g., the h2 JDBC driver checks the timeout of each query instead of an entire JDBC batch. It defaults to 0.
private static final String SOURCE_SCHEMA_JDBC_TIMEOUT = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.timeout";
// If true, all the columns are nullable.
private static final String SOURCE_SCHEMA_JDBC_NULLABLE = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.nullable";
}

public JdbcbasedSchemaProvider(TypedProperties props, JavaSparkContext jssc) {
super(props, jssc);
options.put("url", props.getString(Config.SOURCE_SCHEMA_JDBC_CONNECTION_URL));
options.put("driver", props.getString(Config.SOURCE_SCHEMA_JDBC_DRIVER_TYPE));
options.put("user", props.getString(Config.SOURCE_SCHEMA_JDBC_USERNAME));
options.put("password", props.getString(Config.SOURCE_SCHEMA_JDBC_PASSWORD));
options.put("dbtable", props.getString(Config.SOURCE_SCHEMA_JDBC_DBTABLE));
// the number of seconds the driver will wait for a Statement object to execute to the given
// number of seconds. Zero means there is no limit.
options.put("timeout", props.getString(Config.SOURCE_SCHEMA_JDBC_TIMEOUT, "0"));
options.put("nullable", props.getString(Config.SOURCE_SCHEMA_JDBC_NULLABLE, "true"));
}

@Override
public Schema getSourceSchema() {
if (this.sourceSchema != null) {
return sourceSchema;
}

try {
sourceSchema = UtilHelpers.getJDBCSchema(options);
} catch (Exception e) {
throw new HoodieException("Failed to get Schema through jdbc. ", e);
}
return sourceSchema;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public static void initClass() throws Exception {
props.setProperty("hoodie.datasource.write.partitionpath.field", "not_there");
props.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.file", dfsBasePath + "/source.avsc");
props.setProperty("hoodie.deltastreamer.schemaprovider.target.schema.file", dfsBasePath + "/target.avsc");

// Hive Configs
props.setProperty(DataSourceWriteOptions.HIVE_URL_OPT_KEY(), "jdbc:hive2://127.0.0.1:9999/");
props.setProperty(DataSourceWriteOptions.HIVE_DATABASE_OPT_KEY(), "testdb1");
Expand Down Expand Up @@ -526,7 +527,7 @@ public void testNullSchemaProvider() throws Exception {
assertTrue(e.getMessage().contains("Please provide a valid schema provider class!"));
}
}

@Test
public void testPayloadClassUpdate() throws Exception {
String dataSetBasePath = dfsBasePath + "/test_dataset_mor";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.hudi.utilities;

import org.apache.hudi.common.util.TypedProperties;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.utilities.schema.JdbcbasedSchemaProvider;

import org.apache.avro.Schema;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.spark.api.java.JavaSparkContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import static org.junit.Assert.assertEquals;

public class TestJdbcbasedSchemaProvider {

private static final Logger LOG = LogManager.getLogger(TestJdbcbasedSchemaProvider.class);
private static final TypedProperties PROPS = new TypedProperties();
protected transient JavaSparkContext jsc = null;

@Before
public void init() {
jsc = UtilHelpers.buildSparkContext(this.getClass().getName() + "-hoodie", "local[2]");
PROPS.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.jdbc.connection.url", "jdbc:h2:mem:test_mem");
PROPS.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.jdbc.driver.type", "org.h2.Driver");
PROPS.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.jdbc.username", "sa");
PROPS.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.jdbc.password", "");
PROPS.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.jdbc.dbtable", "triprec");
PROPS.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.jdbc.timeout", "0");
PROPS.setProperty("hoodie.deltastreamer.schemaprovider.source.schema.jdbc.nullable", "false");
}

@After
public void teardown() throws Exception {
if (jsc != null) {
jsc.stop();
}
}

@Test
public void testJdbcbasedSchemaProvider() throws Exception {
try {
initH2Database();
Schema sourceSchema = UtilHelpers.createSchemaProvider(JdbcbasedSchemaProvider.class.getName(), PROPS, jsc).getSourceSchema();
assertEquals(sourceSchema.toString().toUpperCase(), new Schema.Parser().parse(UtilitiesTestBase.Helpers.readFile("delta-streamer-config/source-jdbc.avsc")).toString().toUpperCase());
} catch (HoodieException e) {
LOG.error("Failed to get connection through jdbc. ", e);
}
}

/**
* Initialize the H2 database and obtain a connection, then create a table as a test.
* Based on the characteristics of the H2 in-memory database, we do not need to display the initialized database.
* @throws SQLException
* @throws IOException
*/
private void initH2Database() throws SQLException, IOException {
Connection conn = DriverManager.getConnection("jdbc:h2:mem:test_mem", "sa", "");
PreparedStatement ps = conn.prepareStatement(UtilitiesTestBase.Helpers.readFile("delta-streamer-config/triprec.sql"));
ps.executeUpdate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,20 @@ public static class Helpers {
// to get hold of resources bundled with jar
private static ClassLoader classLoader = Helpers.class.getClassLoader();

public static void copyToDFS(String testResourcePath, FileSystem fs, String targetPath) throws IOException {
public static String readFile(String testResourcePath) throws IOException {
BufferedReader reader =
new BufferedReader(new InputStreamReader(classLoader.getResourceAsStream(testResourcePath)));
PrintStream os = new PrintStream(fs.create(new Path(targetPath), true));
StringBuffer sb = new StringBuffer();
String line;
while ((line = reader.readLine()) != null) {
os.println(line);
sb.append(line + "\n");
}
return sb.toString();
}

public static void copyToDFS(String testResourcePath, FileSystem fs, String targetPath) throws IOException {
PrintStream os = new PrintStream(fs.create(new Path(targetPath), true));
os.print(readFile(testResourcePath));
os.flush();
os.close();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Comment thread
vinothchandar marked this conversation as resolved.
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
{
"type": "record",
"name": "triprec",
"namespace": "hoodie.triprec",
"fields": [
{
"name":"ID",
"type": "int"
},
{
"name": "TIMESTAMP",
"type": ["double", "null"]
},
{
"name": "RIDER",
"type": ["string", "null"]
},
{
"name": "DRIVER",
"type": ["string", "null"]
},
{
"name": "BEGIN_LAT",
"type": ["double", "null"]
},
{
"name": "BEGIN_LON",
"type": ["double", "null"]
},
{
"name": "END_LAT",
"type": ["double", "null"]
},
{
"name": "END_LON",
"type": ["double", "null"]
},
{
"name": "FARE",
"type": ["double", "null"]
} ]
}
Loading