Skip to content

fix(backend): select the storage dialect by driver instead of always MySQL - #13993

Open
hsinhoyeh wants to merge 1 commit into
kubeflow:masterfrom
hsinhoyeh:fix-postgres-storage-dialect
Open

fix(backend): select the storage dialect by driver instead of always MySQL#13993
hsinhoyeh wants to merge 1 commit into
kubeflow:masterfrom
hsinhoyeh:fix-postgres-storage-dialect

Conversation

@hsinhoyeh

Copy link
Copy Markdown
Contributor

Part of #13956. This is a bug I found while scoping items 1 and 2 that the issue does not mention.

initDBClient ends with:

return storage.NewDB(newdb, storage.NewMySQLDialect())

The dialect is hardcoded, so every install gets MySQLDialect regardless of driverName — including PostgreSQL installs, which reach this line through the pgx branch a few lines above.

What that breaks

storage.SQLDialect exists specifically to paper over engine differences, and only MySQLDialect and SQLiteDialect implement it — there is no PostgreSQL implementation at all. So on a PostgreSQL install the stores emit MySQL-only SQL:

Method Emitted (MySQL) PostgreSQL needs
GroupConcat GROUP_CONCAT(x SEPARATOR "y") STRING_AGG(x::text, 'y')
Concat CONCAT(a,"y",b) CONCAT(a,'y',b) — double quotes are an identifier in PG
Upsert ON DUPLICATE KEY UPDATE c=VALUES(c) ON CONFLICT(k) DO UPDATE SET c=EXCLUDED.c
UpdateWithJointOrFrom UPDATE t INNER JOIN u ... UPDATE t SET ... FROM u ...
IsDuplicateError asserts *mysql.MySQLError *pgconn.PgError, SQLSTATE 23505

IsDuplicateError is the quiet one. A pgx error can never satisfy err.(*mysql.MySQLError), so it returns false for every conflict and callers take the "not a duplicate" branch — no error, just wrong behaviour.

Change

  • Add PostgreSQLDialect implementing SQLDialect, plus NewPostgreSQLDialect().
  • Replace the hardcoded call with newStorageDialect(driverName), which rejects unknown drivers with the same message initDBDriver already uses rather than silently defaulting to one engine.

Implementation notes:

  • STRING_AGG requires an explicit separator, unlike GROUP_CONCAT which defaults to ,. An empty separator falls back to , so the SQL keeps parsing and keeps MySQL's behaviour. A single-argument STRING_AGG does not parse; there is a test for this.
  • Separators are single-quoted, since PostgreSQL reads a double-quoted token as an identifier rather than a string literal.
  • IsDuplicateError uses errors.As because the pgx stdlib driver wraps *pgconn.PgError before it surfaces through database/sql. Tested with wrapped, bare, unrelated-SQLSTATE, non-PG, and nil errors.
  • The SQLSTATE is a local pgUniqueViolation constant rather than adding a dependency on jackc/pgerrcode for one value.

What this does not fix

This does not make the PostgreSQL overlay work, and it is not intended to. Still outstanding from #13956:

  • Item 1 — the storage call sites still build with squirrel's default ? placeholders instead of the $1 form the dialect in client_manager/dialect.go already defines.
  • Item 2 — identifier casing.
  • The Concat call sites in job_store.go:220 and run_store.go:270,283,298 pass double-quoted SQL string literals (`"["`, `"]"`) which PostgreSQL reads as identifiers. That is caller-side and left alone here deliberately, rather than writing MySQL-only syntax into a new PostgreSQL dialect.

Those need the design decision discussed in #13956 (comment). This PR removes one independent layer: the stores now receive a dialect matching the engine they are talking to.

Testing

10 new tests in db_test.go covering every PostgreSQLDialect method, both separator branches, both Upsert overwrite branches, and five IsDuplicateError cases. Plus a regression test asserting MySQLDialect.IsDuplicateError returns false for a PostgreSQL error, which is the bug being fixed.

ok  github.com/kubeflow/pipelines/backend/src/apiserver/storage         4.360s
ok  github.com/kubeflow/pipelines/backend/src/apiserver/client_manager  4.804s

Note for reproducing locally: the storage package needs CGO_ENABLED=1, since SQLiteDialect.IsDuplicateError uses mattn/go-sqlite3, which only exposes sqlite3.Error and sqlite3.ErrConstraint under cgo.

I have not run this against a live PostgreSQL instance — the remaining items above still prevent ml-pipeline from reaching Ready, so there is nothing end-to-end to verify against yet.

…MySQL

initDBClient ended with:

    return storage.NewDB(newdb, storage.NewMySQLDialect())

so every install received MySQLDialect regardless of driverName. On a
PostgreSQL install the stores therefore emitted MySQL-only SQL:

  - GroupConcat produced GROUP_CONCAT(... SEPARATOR "x"), which PostgreSQL
    does not have; it spells this STRING_AGG.
  - Upsert produced ON DUPLICATE KEY UPDATE ... VALUES(col) rather than
    ON CONFLICT ... DO UPDATE SET col = EXCLUDED.col.
  - UpdateWithJointOrFrom produced UPDATE ... INNER JOIN rather than
    UPDATE ... FROM.
  - IsDuplicateError type-asserted *mysql.MySQLError, which a pgx error can
    never satisfy, so duplicate-key detection silently returned false for
    every conflict and callers took the "not a duplicate" branch.

Add PostgreSQLDialect implementing the SQLDialect interface, and select the
dialect from driverName in a new newStorageDialect helper. The helper rejects
unknown drivers with the same message initDBDriver uses, rather than silently
defaulting to one engine.

Notes on the implementation:

  - STRING_AGG requires an explicit separator, unlike GROUP_CONCAT which
    defaults to ",". An empty separator therefore falls back to "," so the
    generated SQL keeps parsing and keeps MySQL's behaviour.
  - Separators are single-quoted. PostgreSQL reads a double-quoted token as an
    identifier, not a string literal, so MySQLDialect's CONCAT(a,"x",b) form
    would resolve "x" as a column name.
  - IsDuplicateError matches SQLSTATE 23505 via errors.As, because the pgx
    stdlib driver wraps *pgconn.PgError before it surfaces through
    database/sql. The SQLSTATE is a local constant rather than a new
    dependency on jackc/pgerrcode for a single value.

This does not by itself make the PostgreSQL overlay work. The placeholder
dialect (item 1) and the identifier casing (item 2) in kubeflow#13956 are unchanged,
and the Concat call sites in job_store.go and run_store.go still pass
double-quoted SQL string literals that PostgreSQL will read as identifiers.
Those are tracked separately. This commit removes one layer: the stores now at
least receive a dialect that matches the engine they are talking to.

Part of kubeflow#13956

Signed-off-by: hsinhoyeh <yhh92u@gmail.com>
@google-oss-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign hbelmiro for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@google-oss-prow

Copy link
Copy Markdown

Hi @hsinhoyeh. Thanks for your PR.

I'm waiting for a kubeflow member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

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.

1 participant