Skip to content

feat: Support PostgreSQL export for export-metadata#19698

Open
JWuCines wants to merge 4 commits into
apache:masterfrom
JWuCines:feature/support_postgres_export_metadata
Open

feat: Support PostgreSQL export for export-metadata#19698
JWuCines wants to merge 4 commits into
apache:masterfrom
JWuCines:feature/support_postgres_export_metadata

Conversation

@JWuCines

@JWuCines JWuCines commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Description

The export-metadata tool currently only supports exporting from Derby metadata stores. This PR adds support for exporting from PostgreSQL by implementing a generic JDBC-based exportTable in SQLMetadataConnector, which PostgreSQL (and any future connector) inherits automatically.

Added generic JDBC export in SQLMetadataConnector

Implemented exportTableWithJdbc, a protected method that exports any table to CSV using standard JDBC. Binary/BLOB columns (including PostgreSQL BYTEA) are hex-encoded, booleans are written as true/false strings, and string values containing commas, quotes, \n, or \r are properly CSV-escaped per RFC 4180. The base exportTable delegates to this method, while DerbyConnector continues to override it with Derby's native SYSCS_EXPORT_TABLE.

Updated ExportMetadata for PostgreSQL compatibility

  • Table names are no longer unconditionally uppercased; uppercasing is now applied only for Derby (detected via jdbc:derby URI prefix), since PostgreSQL uses lowercase table names.
  • Updated the @Command description to mention PostgreSQL support.

Added unit tests

Four new tests in SQLMetadataConnectorTest exercise the generic JDBC export path via TestDerbyConnector.exportTableGeneric():

  • testExportTable — verifies hex-encoded BLOBs and true/false boolean strings
  • testExportTableWithSpecialCharacters — verifies CSV quoting/escaping for commas, double quotes, and plain values
  • testExportTableWithNullValues — verifies NULL columns produce empty CSV fields
  • testExportTablePreservesAllColumns — verifies all columns (including nullable trailing columns like used_status_last_updated) are exported

Updated documentation

  • export-metadata.md — removed the Derby-only limitation, added a "PostgreSQL" section under "Running the tool" with the required -Ddruid.extensions.loadList and -Ddruid.metadata.storage.type flags, clarified _raw.csv description
  • metadata-migration.md — updated intro and export tool reference to include PostgreSQL
  • deep-storage-migration.md — updated export tool reference, added note about no running processes needed when migrating from PostgreSQL

Release note

The export-metadata tool now supports exporting from PostgreSQL metadata stores in addition to Derby. When exporting from PostgreSQL, pass -Ddruid.extensions.loadList='["postgresql-metadata-storage"]' -Ddruid.metadata.storage.type=postgresql on the command line along with the appropriate --connectURI.

Fixed streaming, column preservation, and CSV escaping in export

  • SQLMetadataConnector.exportTableWithJdbc — switched from retryWithHandle (auto-commit) to retryTransaction so the connection runs with autoCommit=false, and applied getStreamingFetchSize() to the Statement. This enables PostgreSQL cursor-based streaming instead of buffering the entire ResultSet in memory.
  • ExportMetadata.rewriteSegmentsExport — the rewrite stage previously hardcoded columns 0–8, dropping used_status_last_updated, indexing_state_fingerprint, upgraded_from_segment_id, and optional schema columns. Added a loop to pass through all columns after payload.
  • ExportMetadata rewrite methods — all five rewrite methods (rewriteDatasourceExport, rewriteRulesExport, rewriteConfigExport, rewriteSupervisorExport, rewriteSegmentsExport) now re-escape non-payload fields via a new csvEscapeField() helper (RFC 4180). Previously, fields containing commas or double quotes were parsed correctly by opencsv but written back without quoting, producing malformed output.
  • ExportMetadataTest — added 10 tests covering csvEscapeField, segments rewrite with all columns, special characters round-trip, and backward compatibility with 9-column tables.

Key changed/added classes in this PR
  • SQLMetadataConnector — added exportTable and exportTableWithJdbc for generic JDBC CSV export; switched export to transactional streaming
  • ExportMetadata — added isDerby() helper, conditional table name casing, csvEscapeField(), all-column preservation in segments rewrite
  • TestDerbyConnector — added exportTableGeneric() to test the generic JDBC path
  • SQLMetadataConnectorTest — added 4 export tests
  • ExportMetadataTest — added 10 tests for CSV escaping and segments rewrite

This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • a release note entry in the PR description.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.

@JWuCines
JWuCines force-pushed the feature/support_postgres_export_metadata branch from b69c7a6 to baa141e Compare July 16, 2026 13:53

@FrankChen021 FrankChen021 left a comment

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.

Severity Findings
P0 0
P1 2
P2 1
P3 0
Total 3

Reviewed 7 of 7 changed files.

Found three export-migration risks: buffered PostgreSQL reads, dropped current-schema segment columns, and lost CSV escaping during rewrites.


This is an automated review by Codex GPT-5.6-Sol

{
retryWithHandle(
(HandleCallback<Void>) handle -> {
try (Statement stmt = handle.getConnection().createStatement();

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.

[P1] Stream PostgreSQL results instead of buffering the table

PostgreSQL JDBC buffers the complete ResultSet by default. This plain Statement runs on an auto-commit connection and never sets a fetch size; PostgreSQL cursor-based fetching requires autoCommit=false and a positive fetch size. Exporting a production segments table can therefore exhaust heap before rows are written. Run the query in a transaction and apply getStreamingFetchSize().

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.

Fixed in 6b73b7e

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.

Confirmed: retryTransaction gives PostgreSQL an auto-commit-disabled connection, and getStreamingFetchSize() is applied before query execution, enabling cursor-based fetching. Reviewed 8 of 8 changed files.

final String exportTableName = isDerby() ? StringUtils.toUpperCase(tableName) : tableName;
dbConnector.exportTable(
StringUtils.toUpperCase(tableName),
exportTableName,

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.

[P1] Preserve current segment columns in the importable CSV

The generic raw export preserves all columns, but run() immediately passes the file through rewriteSegmentsExport, which emits only columns 0 through 8. Current segment tables also contain required used_status_last_updated and additional fingerprint/upgrade columns. A PostgreSQL migration therefore loses those values, and the documented COPY into a freshly created current table fails because used_status_last_updated is NOT NULL without a default. The new test covers only the raw file; the final CSV and import commands need to preserve the current schema.

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.

Fixed in 6b73b7e

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.

Thanks; the rewrite now preserves trailing values, but the documented PostgreSQL COPY druid_segments(...) command still names only the original nine columns. Current exports contain 12–14 fields, so COPY fails with extra data; upgraded source tables can also have added columns in a different physical order. Please export segments in an explicit stable column order and update the import column lists accordingly. Reviewed 8 of 8 changed files.

} else {
final String val = rs.getString(i);
if (val != null) {
if (val.contains(",") || val.contains("\"") || val.contains("\n") || val.contains("\r")) {

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.

[P2] Preserve CSV escaping through the rewrite stage

Although the raw writer correctly quotes commas and double quotes, each ExportMetadata rewrite parses those fields and concatenates them into the final CSV without re-escaping. Legal datasource and metadata identifiers may contain these characters, so the final importable file gains extra columns or malformed quoting even though the raw file is valid. Use a CSV writer or shared escaping routine in the rewrite stage and add an end-to-end test.

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.

Fixed in 6b73b7e

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.

The comma/quote round-trip is fixed, but multiline fields are still split because each rewrite uses BufferedReader.readLine() plus CSVParser.parseLine() on one physical line. exportTableWithJdbc writes quoted newline or carriage-return values as CSV records spanning lines, so the rewrite misparses them before csvEscapeField runs. Please use a record-aware CSVReader/readNext path and cover this end to end. Reviewed 8 of 8 changed files.

Comment thread services/src/test/java/org/apache/druid/cli/ExportMetadataTest.java Fixed
@JWuCines
JWuCines requested a review from FrankChen021 July 20, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants