Skip to content

PostgreSQL UDF Support - #4240

Open
c2nes wants to merge 14 commits into
GoogleCloudPlatform:mainfrom
c2nes:pg-udf
Open

c2nes wants to merge 14 commits into
GoogleCloudPlatform:mainfrom
c2nes:pg-udf

Conversation

@c2nes

@c2nes c2nes commented Sep 9, 2026

Copy link
Copy Markdown

Adding support for UDFs in the PostgreSQL dialect.

Note, this PR is based on the previously reviewed #3325.

This PR has been rebased, review comments addressed, and a few test failures addressed.

MnkyGns and others added 14 commits September 9, 2026 14:09
…lity

This change updates the UdfParameter.parse method to use the database dialect for determining the correct quote character (backticks for GoogleSQL, double quotes for PostgreSQL). It also refactors the string processing logic to avoid manual index manipulation, leveraging regex-based splitting for the DEFAULT expression.

Additionally, it includes a fix for a compilation error in DirectRunnerClient.java by replacing a deprecated/missing stop() call with interrupt().

New unit tests have been added to verify correct parsing of quoted identifiers and default expressions across dialects.
This change adds integration tests to ExportPipelineIT and ImportPipelineIT
to verify that User-Defined Functions (UDFs) are correctly exported and
imported across different Spanner dialects (GoogleSQL and PostgreSQL).

- Added UDF definitions to test DDL files.
- Updated ExportPipelineIT to verify UDF artifacts in GCS.
- Added testPostgresImportPipeline_UDF to ImportPipelineIT to verify
  UDF restoration from Avro artifacts.
- Included necessary Avro and manifest resources for the import test.
- Replaced manual index manipulation with regex in UdfParameter.parse for better readability and robust dialect support.
- Added support for PostgreSQL types with spaces (e.g., 'double precision') in UdfParameter.parse.
- Preserved GoogleSQL single-word type restriction to minimize behavioral changes.
- Added unit tests for PostgreSQL types with spaces.
- Fixed unused import in InformationSchemaScannerTest.java.
- Delete pr_comments.json per PR review feedback.
- Fix Ddl.Builder.createUdf to pass dialect to Udf.builder(dialect) so scanned PostgreSQL UDFs retain Dialect.POSTGRESQL.
- Fix PostgreSQL UDF parameter quoting and determinism assertions in ExportPipelineIT.
- Add missing pg_multiply.avro resource and fix manifest filenames and Base64 MD5 checksums for ImportPipelineIT.
- Reintroduce PostgreSQL UDF coverage in CopyDbIT-AllSchemaAndData-pg.sql and AvroSchemaToDdlConverterTest.

TAG=agy
CONV=f1c413a3-3f8a-43cd-8eac-9b7a2df210e0
@c2nes
c2nes requested a review from a team as a code owner September 9, 2026 16:40
@google-cla

google-cla Bot commented Sep 9, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces support for User-Defined Functions (UDFs) within the PostgreSQL dialect for Spanner Dataflow templates. It enables the export and import of PostgreSQL UDFs by mapping determinism levels to volatility keywords and enhancing parameter parsing to handle quoted identifiers and default values. These changes ensure consistency and compatibility when migrating or exporting Spanner databases using the PostgreSQL dialect.

Highlights

  • PostgreSQL UDF Support: Added full support for User-Defined Functions (UDFs) in the PostgreSQL dialect for Spanner Dataflow templates.
  • Determinism Mapping: Implemented mapping for spannerDeterminism to PostgreSQL volatility keywords (IMMUTABLE, STABLE, VOLATILE).
  • Parameter Parsing: Enhanced UDF parameter parsing to correctly handle quoted identifiers and default values in PostgreSQL.
  • Information Schema Integration: Updated the InformationSchemaScanner to extract PostgreSQL UDF metadata, including determinism and parameter details.
  • Testing: Added comprehensive unit and integration tests to verify end-to-end export and import workflows for PostgreSQL UDFs.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request adds support for User-Defined Functions (UDFs) in the PostgreSQL dialect for Cloud Spanner, including metadata extraction, Avro schema conversion, and SQL generation. The review feedback highlights critical improvements to the parsing logic in UdfParameter.parse, such as fixing a regex bug where Pattern.quote is incorrectly used inside a character class, and addressing a split-logic issue that could bypass empty default expression checks. Additionally, suggestions are made to increase robustness by trimming input parameters and handling case-insensitivity for determinism values. All comments are highly actionable and should be addressed.

Comment on lines +60 to +62
String regex =
String.format(
"^((%1$s[^%1$s]+%1$s)|(\\S+))\\s+(.*)$", java.util.regex.Pattern.quote(quote));

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.

high

Using Pattern.quote(quote) inside the character class [^%1$s] is problematic because Pattern.quote wraps the string in \Q and \E. Inside a regex character class [...], \Q and \E are treated as literal characters in many regex engines or can cause unexpected matching behavior (e.g., failing to match identifiers containing 'Q' or 'E'). Since quote is always a single character (double quote or backtick), you can safely use it directly without quoting.

Suggested change
String regex =
String.format(
"^((%1$s[^%1$s]+%1$s)|(\\S+))\\s+(.*)$", java.util.regex.Pattern.quote(quote));
String regex =
String.format(
"^((%1$s[^%1$s]+%1$s)|(\\S+))\\s+(.*)$", quote);

Comment on lines 52 to +53
public static UdfParameter parse(String parameter, String functionSpecificName, Dialect dialect) {
String[] paramParts = parameter.split(" ");
if (paramParts.length < 2) {
String quote = identifierQuote(dialect);

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.

medium

Trimming the parameter string at the beginning of parse ensures that any leading or trailing whitespace is robustly handled, preventing unexpected parsing failures.

Suggested change
public static UdfParameter parse(String parameter, String functionSpecificName, Dialect dialect) {
String[] paramParts = parameter.split(" ");
if (paramParts.length < 2) {
String quote = identifierQuote(dialect);
public static UdfParameter parse(String parameter, String functionSpecificName, Dialect dialect) {
parameter = parameter.trim();
String quote = identifierQuote(dialect);

Comment on lines +75 to +78
// Split 'rest' into type and defaultExpression using a case-insensitive 'DEFAULT' keyword.
String[] parts = rest.split("(?i)\\s+DEFAULT\\s+", 2);
String type = parts[0].trim();
String defaultExpression = parts.length > 1 ? parts[1].trim() : null;

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.

medium

Using (?i)\s+DEFAULT\s+ as the split regex requires at least one whitespace character after the DEFAULT keyword. If a parameter ends with DEFAULT (e.g., p1 int32 DEFAULT), the regex won't match, resulting in defaultExpression being null and bypassing the empty default check. Changing the regex to (?i)\s+DEFAULT\b ensures it splits correctly even when there is no trailing expression, making the defaultExpression.isEmpty() validation reachable and robust.

Suggested change
// Split 'rest' into type and defaultExpression using a case-insensitive 'DEFAULT' keyword.
String[] parts = rest.split("(?i)\\s+DEFAULT\\s+", 2);
String type = parts[0].trim();
String defaultExpression = parts.length > 1 ? parts[1].trim() : null;
// Split 'rest' into type and defaultExpression using a case-insensitive 'DEFAULT' keyword.
String[] parts = rest.split("(?i)\\s+DEFAULT\\b", 2);
String type = parts[0].trim();
String defaultExpression = parts.length > 1 ? parts[1].trim() : null;

Comment on lines +120 to +121
} else if (spannerDeterminism() != null && dialect() == Dialect.POSTGRESQL) {
switch (spannerDeterminism()) {

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.

medium

To ensure robustness against different casing of the determinism string (e.g., if retrieved from external sources or metadata), it is safer to perform a case-insensitive comparison by converting spannerDeterminism() to uppercase before matching.

Suggested change
} else if (spannerDeterminism() != null && dialect() == Dialect.POSTGRESQL) {
switch (spannerDeterminism()) {
} else if (spannerDeterminism() != null && dialect() == Dialect.POSTGRESQL) {
switch (spannerDeterminism().toUpperCase()) {

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.48649% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.05%. Comparing base (d3d4c76) to head (18d4468).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ava/com/google/cloud/teleport/spanner/ddl/Udf.java 71.42% 2 Missing and 2 partials ⚠️
...oogle/cloud/teleport/spanner/ddl/UdfParameter.java 94.44% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #4240      +/-   ##
============================================
+ Coverage     55.85%   56.05%   +0.20%     
+ Complexity     7488     7135     -353     
============================================
  Files          1135     1139       +4     
  Lines         70313    70793     +480     
  Branches       8040     8091      +51     
============================================
+ Hits          39271    39685     +414     
- Misses        28490    28540      +50     
- Partials       2552     2568      +16     
Components Coverage Δ
spanner-templates 84.22% <ø> (+0.02%) ⬆️
spanner-import-export 69.00% <86.48%> (+0.10%) ⬆️
spanner-live-forward-migration 88.64% <ø> (-0.02%) ⬇️
spanner-live-reverse-replication 80.37% <ø> (+<0.01%) ⬆️
spanner-bulk-migration 88.91% <ø> (-0.16%) ⬇️
gcs-spanner-dv 87.96% <ø> (-0.02%) ⬇️
Files with missing lines Coverage Δ
...oud/teleport/spanner/AvroSchemaToDdlConverter.java 88.44% <100.00%> (+1.16%) ⬆️
...va/com/google/cloud/teleport/spanner/AvroUtil.java 93.75% <ø> (ø)
...oud/teleport/spanner/DdlToAvroSchemaConverter.java 97.56% <100.00%> (+0.01%) ⬆️
...ava/com/google/cloud/teleport/spanner/ddl/Ddl.java 77.50% <100.00%> (ø)
...oogle/cloud/teleport/spanner/ddl/UdfParameter.java 80.85% <94.44%> (+1.30%) ⬆️
...ava/com/google/cloud/teleport/spanner/ddl/Udf.java 83.33% <71.42%> (-0.83%) ⬇️

... and 13 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants