Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,14 @@ data should the <tt>COPY</tt> fail.</p>
Redshift cluster and/or don't have requirements to keep the table availability high.</p>
</td>
</tr>
<tr>
<td><tt>description</tt></td>
<td>No</td>
<td>No default</td>
<td>
<p>A description for the table. Will be set using the SQL COMMENT command, and should show up in most query tools.
See also the <tt>description</tt> metadata to set descriptions on individual columns.
</tr>
<tr>
<td><tt>preactions</tt></td>
<td>No</td>
Expand Down Expand Up @@ -414,6 +422,14 @@ df.write
.save()
```

### Configuring column encoding

When creating a table, this library can be configured to use a specific compression encoding on individual columns. You can use the `encoding` column metadata field to specify a compression encoding for each column (see [Amazon docs](http://docs.aws.amazon.com/redshift/latest/dg/c_Compression_encodings.html) for available encodings).

### Setting descriptions on columns

Redshift allows columns to have descriptions attached that should show up in most query tools (using the `COMMENT` command). You can set the `description` column metadata field to specify a description for individual columns.

## Transactional Guarantees

This section describes `spark-redshift`'s transactional guarantees.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,93 @@ class RedshiftIntegrationSuite extends IntegrationSuiteBase {
}
}

test("configuring compression on columns") {
val tableName = s"configuring_compression_on_columns_$randomSuffix"
try {
val metadata = new MetadataBuilder().putString("encoding", "LZO").build()
val schema = StructType(
StructField("x", StringType, metadata = metadata) :: Nil)
sqlContext.createDataFrame(sc.parallelize(Seq(Row("a" * 128))), schema).write
.format("com.databricks.spark.redshift")
.option("url", jdbcUrl)
.option("dbtable", tableName)
.option("tempdir", tempDir)
.mode(SaveMode.ErrorIfExists)
.save()
assert(DefaultJDBCWrapper.tableExists(conn, tableName))
val loadedDf = sqlContext.read
.format("com.databricks.spark.redshift")
.option("url", jdbcUrl)
.option("dbtable", tableName)
.option("tempdir", tempDir)
.load()
checkAnswer(loadedDf, Seq(Row("a" * 128)))
val encodingDF = sqlContext.read
.format("com.databricks.spark.redshift")
.option("url", jdbcUrl)
.option("dbtable",
s"""(SELECT "column", encoding FROM pg_table_def WHERE tablename='$tableName')""")

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.

Interestingly, this query is now failing for me with the following error:

[info]   Caused by: com.amazon.support.exceptions.ErrorException: [Amazon](500310) Invalid operation: Specified types or functions (one per INFO message) not supported on Redshift tables.;
[info]      ... 28 more (QueryTest.scala:60)
[info]   org.scalatest.exceptions.TestFailedException:
[info]   at org.scalatest.Assertions$class.newAssertionFailedException(Assertions.scala:495)
[info]   at org.scalatest.FunSuite.newAssertionFailedException(FunSuite.scala:1555)
[info]   at org.scalatest.Assertions$class.fail(Assertions.scala:1328)
[info]   at org.scalatest.FunSuite.fail(FunSuite.scala:1555)
[info]   at com.databricks.spark.redshift.QueryTest$class.checkAnswer(QueryTest.scala:60)
[info]   at com.databricks.spark.redshift.RedshiftIntegrationSuite.checkAnswer(RedshiftIntegrationSuite.scala:27)
[info]   at com.databricks.spark.redshift.RedshiftIntegrationSuite$$anonfun$17.apply$mcV$sp(RedshiftIntegrationSuite.scala:417)
[info]   at com.databricks.spark.redshift.RedshiftIntegrationSuite$$anonfun$17.apply(RedshiftIntegrationSuite.scala:389)
[info]   at com.databricks.spark.redshift.RedshiftIntegrationSuite$$anonfun$17.apply(RedshiftIntegrationSuite.scala:389)
[info]   at org.scalatest.Transformer$$anonfun$apply$1.apply$mcV$sp(Transformer.scala:22)
[info]   at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
[info]   at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
[info]   at org.scalatest.Transformer.apply(Transformer.scala:22)
[info]   at org.scalatest.Transformer.apply(Transformer.scala:20)
[info]   at org.scalatest.FunSuiteLike$$anon$1.apply(FunSuiteLike.scala:166)
[info]   at org.scalatest.Suite$class.withFixture(Suite.scala:1122)
[info]   at org.scalatest.FunSuite.withFixture(FunSuite.scala:1555)
[info]   at org.scalatest.FunSuiteLike$class.invokeWithFixture$1(FunSuiteLike.scala:163)
[info]   at org.scalatest.FunSuiteLike$$anonfun$runTest$1.apply(FunSuiteLike.scala:175)
[info]   at org.scalatest.FunSuiteLike$$anonfun$runTest$1.apply(FunSuiteLike.scala:175)
[info]   at org.scalatest.SuperEngine.runTestImpl(Engine.scala:306)
[info]   at org.scalatest.FunSuiteLike$class.runTest(FunSuiteLike.scala:175)
[info]   at com.databricks.spark.redshift.RedshiftIntegrationSuite.org$scalatest$BeforeAndAfterEach$$super$runTest(RedshiftIntegrationSuite.scala:27)
[...]

I received a bug report from a user who hit this problem but was never able to get to the bottom of it as far as I could tell (since I didn't have access to any logs or queries to even know which query was triggering that error). I'm going to see if I can add some additional logging to help figure out what's going on here now that I've stumbled across a reproduction.

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.

Err, here's a better copy of the stacktrace:

[info]   java.sql.SQLException: [Amazon](500310) Invalid operation: Specified types or functions (one per INFO message) not supported on Redshift tables.;
[info]      at com.amazon.redshift.client.messages.inbound.ErrorResponse.toErrorException(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.handleErrorResponse(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getOperationMetadata(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getOperationMetadata(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.handleMessage(Unknown Source)
[info]      at com.amazon.jdbc.communications.InboundMessagesPipeline.getNextMessageOfClass(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.doMoveToNextClass(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getReadyForQuery(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getOperationMetadata(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getOperationMetadata(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.handleMessage(Unknown Source)
[info]      at com.amazon.jdbc.communications.InboundMessagesPipeline.getNextMessageOfClass(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.doMoveToNextClass(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getReadyForQuery(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getOperationMetadata(Unknown Source)
[info]      at com.amazon.redshift.client.PGMessagingContext.getOperationMetadata(Unknown Source)
[info]      at com.amazon.redshift.dataengine.PGExecutionResults.<init>(Unknown Source)
[info]      at com.amazon.redshift.dataengine.PGAbstractQueryExecutor.createExecutionResults(Unknown Source)
[info]      at com.amazon.redshift.dataengine.PGAbstractQueryExecutor.getResults(Unknown Source)
[info]      at com.amazon.jdbc.common.SPreparedStatement.executeWithParams(Unknown Source)
[info]      at com.amazon.jdbc.common.SPreparedStatement.execute(Unknown Source)
[info]      at com.databricks.spark.redshift.JDBCWrapper$$anonfun$executeInterruptibly$1.apply(RedshiftJDBCWrapper.scala:122)
[info]      at com.databricks.spark.redshift.JDBCWrapper$$anonfun$executeInterruptibly$1.apply(RedshiftJDBCWrapper.scala:122)
[info]      at com.databricks.spark.redshift.JDBCWrapper$$anonfun$2.apply(RedshiftJDBCWrapper.scala:140)
[info]      at scala.concurrent.impl.Future$PromiseCompletingRunnable.liftedTree1$1(Future.scala:24)
[info]      at scala.concurrent.impl.Future$PromiseCompletingRunnable.run(Future.scala:24)
[info]      at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
[info]      at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

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.

This is a client-side error; I added ?loglevel=1 to the end of my JDBC URL in order to get the Redshift JDBC driver to perform more logging and used DriverManager.setLogWriter(new PrintWriter(System.out)) to direct those logs to stdout.

This produced the following output:

SQLWarning: reason(Function "format_type(oid,integer)" not supported.) SQLState(01000) vendor code(0)
SQLWarning: reason(Function "pg_table_is_visible(oid)" not supported.) SQLState(01000) vendor code(0)

It looks like what's happening here is that we're trying to perform an UNLOAD on a leader-only table, which is unsupported: https://stackoverflow.com/questions/28719808/how-to-unload-pg-table-def-table-to-s3

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 what you'll have to do here is to use the regular JDBC data source to query these Redshift system tables rather than using spark-redshift.

.option("tempdir", tempDir)
.load()
checkAnswer(encodingDF, Seq(Row("x", "LZO")))
} finally {
conn.prepareStatement(s"drop table if exists $tableName").executeUpdate()
conn.commit()
}
}

test("configuring comments on columns") {
val tableName = s"configuring_comments_on_columns_$randomSuffix"
try {
val metadata = new MetadataBuilder().putString("description", "Hello Column").build()
val schema = StructType(
StructField("x", StringType, metadata = metadata) :: Nil)
sqlContext.createDataFrame(sc.parallelize(Seq(Row("a" * 128))), schema).write
.format("com.databricks.spark.redshift")
.option("url", jdbcUrl)
.option("dbtable", tableName)
.option("tempdir", tempDir)
.mode(SaveMode.ErrorIfExists)
.save()
assert(DefaultJDBCWrapper.tableExists(conn, tableName))
val loadedDf = sqlContext.read
.format("com.databricks.spark.redshift")
.option("url", jdbcUrl)
.option("dbtable", tableName)
.option("tempdir", tempDir)
.option("description", "Hello Table")

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.

The "description" field isn't relevant for loads. Did you mean to put this up on line 434 instead?

.load()
checkAnswer(loadedDf, Seq(Row("a" * 128)))
val tableDF = sqlContext.read
.format("com.databricks.spark.redshift")
.option("url", jdbcUrl)
.option("dbtable", s"(SELECT pg_catalog.obj_description('$tableName'::regclass))")
.option("tempdir", tempDir)
.load()
checkAnswer(tableDF, Seq(Row("Hello Table")))
val commentQuery =
s"""
|(SELECT c.column_name, pgd.description
|FROM pg_catalog.pg_statio_all_tables st
|INNER JOIN pg_catalog.pg_description pgd
| ON (pgd.objoid=st.relid)
|INNER JOIN information_schema.columns c
| ON (pgd.objsubid=c.ordinal_position AND c.table_name=st.relname)
|WHERE c.table_name='$tableName')
""".stripMargin;

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.

Semicolon is unnecessary.

val columnDF = sqlContext.read
.format("com.databricks.spark.redshift")
.option("url", jdbcUrl)
.option("dbtable", commentQuery)
.option("tempdir", tempDir)
.load()
checkAnswer(columnDF, Seq(Row("x", "Hello Column")))
} finally {
conn.prepareStatement(s"drop table if exists $tableName").executeUpdate()
conn.commit()
}
}

test("informative error message when saving a table with string that is longer than max length") {
val tableName = s"error_message_when_string_too_long_$randomSuffix"
try {
Expand Down
5 changes: 5 additions & 0 deletions src/main/scala/com/databricks/spark/redshift/Parameters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ private[redshift] object Parameters {
*/
def extraCopyOptions: String = parameters.get("extracopyoptions").getOrElse("")

/**
* Description of the table, set using the SQL COMMENT command.
*/
def description: Option[String] = parameters.get("description")

/**
* List of semi-colon separated SQL statements to run before write operations.
* This can be useful for running DELETE operations to clean up data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,12 @@ private[redshift] class JDBCWrapper {
case _ => throw new IllegalArgumentException(s"Don't know how to save $field to JDBC")
}
val nullable = if (field.nullable) "" else "NOT NULL"
sb.append(s""", "${name.replace("\"", "\\\"")}" $typ $nullable""".trim)
val encoding = if (field.metadata.contains("encoding")) {
s"ENCODE ${field.metadata.getString("encoding")}"
} else {
""
}
sb.append(s""", "${name.replace("\"", "\\\"")}" $typ $nullable $encoding""".trim)
}}
if (sb.length < 2) "" else sb.substring(2)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ private[redshift] class RedshiftWriter(
}
}

/**
* Generate COMMENT SQL statements for the table and columns.
*/
private[redshift] def commentActions(tableComment: Option[String], schema: StructType):
List[String] = {
tableComment.toList.map(desc => s"COMMENT ON TABLE %s IS '${desc.replace("'", "''")}'") ++
schema.fields
.withFilter(f => f.metadata.contains("description"))
.map(f => s"""COMMENT ON COLUMN %s."${f.name.replace("\"", "\\\"")}""""
+ s" IS '${f.metadata.getString("description").replace("'", "''")}'")
}

/**
* Perform the Redshift load, including deletion of existing data in the case of an overwrite,
* and creating the table if it doesn't already exist.
Expand All @@ -161,8 +173,10 @@ private[redshift] class RedshiftWriter(
log.info(createStatement)
jdbcWrapper.executeInterruptibly(conn.prepareStatement(createStatement))

val preActions = commentActions(params.description, data.schema) ++ params.preActions

// Execute preActions
params.preActions.foreach { action =>
preActions.foreach { action =>
val actionSql = if (action.contains("%s")) action.format(params.table.get) else action
log.info("Executing preAction: " + actionSql)
jdbcWrapper.executeInterruptibly(conn.prepareStatement(actionSql))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,41 @@ class RedshiftSourceSuite
assert(createTableCommand === expectedCreateTableCommand)
}

test("configuring encoding on columns") {
val lzoMetadata = new MetadataBuilder().putString("encoding", "LZO").build()
val runlengthMetadata = new MetadataBuilder().putString("encoding", "RUNLENGTH").build()
val schema = StructType(
StructField("lzo_str", StringType, metadata = lzoMetadata) ::
StructField("runlength_str", StringType, metadata = runlengthMetadata) ::
StructField("default_str", StringType) ::
Nil)
val df = testSqlContext.createDataFrame(sc.emptyRDD[Row], schema)
val createTableCommand =
DefaultRedshiftWriter.createTableSql(df, MergedParameters.apply(defaultParams)).trim
val expectedCreateTableCommand =
"""CREATE TABLE IF NOT EXISTS "PUBLIC"."test_table" ("lzo_str" TEXT ENCODE LZO,""" +
""" "runlength_str" TEXT ENCODE RUNLENGTH, "default_str" TEXT)"""
assert(createTableCommand === expectedCreateTableCommand)
}

test("configuring descriptions on columns") {
val descriptionMetadata1 = new MetadataBuilder().putString("description", "Test1").build()
val descriptionMetadata2 = new MetadataBuilder().putString("description", "Test'2").build()
val schema = StructType(
StructField("first_str", StringType, metadata = descriptionMetadata1) ::
StructField("second_str", StringType, metadata = descriptionMetadata2) ::
StructField("default_str", StringType) ::
Nil)
val df = testSqlContext.createDataFrame(sc.emptyRDD[Row], schema)
val commentCommands =
DefaultRedshiftWriter.commentActions(Some("Test"), schema)
val expectedCommentCommands = List(
"COMMENT ON TABLE %s IS 'Test'",
"COMMENT ON COLUMN %s.\"first_str\" IS 'Test1'",
"COMMENT ON COLUMN %s.\"second_str\" IS 'Test''2'")
assert(commentCommands === expectedCommentCommands)
}

test("Respect SaveMode.ErrorIfExists when table exists") {
val mockRedshift = new MockRedshift(
defaultParams("url"),
Expand Down