Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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.
* @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));
try {
statement.setQueryTimeout(Integer.parseInt(options.get("timeout")));
ResultSet rs = statement.executeQuery();
try {
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);
} finally {
rs.close();
}
} finally {
statement.close();
}
} finally {
conn.close();
}

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.

Would be changed to try-with-resources? since try-catch-finnaly has been optimized to try-with-resources in the project now.

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.

thank you for your advice. i wll do it.

} else {
throw new HoodieException(String.format("%s table does not exists!", table));
}
}

@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,83 @@
/*
* 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.
private static final String TARGET_SCHEMA_JDBC_DRIVER_TYPE = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.driver.type";
private static final String TARGET_SCHEMA_JDBC_USERNAME = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.username";
private static final String TARGET_SCHEMA_JDBC_PASSWORD = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.password";
private static final String TARGET_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 TARGET_SCHEMA_JDBC_TIMEOUT = "hoodie.deltastreamer.schemaprovider.source.schema.jdbc.timeout";
// If true, all the columns are nullable.
private static final String TARGET_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.TARGET_SCHEMA_JDBC_DRIVER_TYPE));
options.put("user", props.getString(Config.TARGET_SCHEMA_JDBC_USERNAME));
options.put("password", props.getString(Config.TARGET_SCHEMA_JDBC_PASSWORD));
options.put("dbtable", props.getString(Config.TARGET_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.TARGET_SCHEMA_JDBC_TIMEOUT, "0"));
options.put("nullable", props.getString(Config.TARGET_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,82 @@
/*
* 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);
}
}

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