From eacd7c3c1e10b3c9d65d0154313f6566b5a05bbc Mon Sep 17 00:00:00 2001 From: Charles Fineman Date: Sun, 19 Jul 2026 19:37:28 +0000 Subject: [PATCH 1/2] fix(query): honor dialect-specific SQL syntax --- dialect/dialect.go | 32 +++++++++++------ dialect/dialect_test.go | 7 +++- docs/guide/mutations.md | 4 +-- docs/index.md | 2 +- docs/reference/dialects.md | 13 ++++++- docs/spec/dialects.md | 3 +- docs/spec/query-builder.md | 2 +- query/insert.go | 5 +++ query/query_test.go | 70 +++++++++++++++++++++++++++++--------- query/select.go | 8 ++--- 10 files changed, 107 insertions(+), 39 deletions(-) diff --git a/dialect/dialect.go b/dialect/dialect.go index a34c812..819877a 100644 --- a/dialect/dialect.go +++ b/dialect/dialect.go @@ -41,6 +41,11 @@ type Dialect interface { // UpsertStyle returns the dialect's INSERT conflict-resolution style. UpsertStyle() UpsertStyle + // SupportsOnConflictConstraint reports whether ON CONFLICT may target a + // named constraint with ON CONSTRAINT. PostgreSQL supports this extension; + // MySQL and SQLite do not. + SupportsOnConflictConstraint() bool + // InsertIgnoreClause returns the SQL keyword phrase that replaces "INSERT" // for an ignore-on-conflict insert, e.g. "INSERT IGNORE" (MySQL) or // "INSERT OR IGNORE" (SQLite). Returns "" for dialects that have no native @@ -77,10 +82,9 @@ type Dialect interface { // True for PostgreSQL and MySQL; false for SQLite, which uses file-level // locking only. // - // Note: the shared-lock syntax differs by dialect — PostgreSQL uses FOR SHARE - // while MySQL uses LOCK IN SHARE MODE (see ForShareClause). The query builder - // gates all row-level locking clauses on this flag; when false, requested - // locking returns an unsupported_feature build error. + // PostgreSQL and MySQL 8.0+ use FOR SHARE for shared row locks. The query + // builder gates all row-level locking clauses on this flag; when false, + // requested locking returns an unsupported_feature build error. SupportsForUpdate() bool // SupportsForNoKeyUpdate reports whether the dialect supports the @@ -107,15 +111,15 @@ type Dialect interface { SupportsRightJoin() bool // ForShareClause returns the SQL keyword phrase for a shared row lock. - // PostgreSQL: "FOR SHARE". MySQL: "LOCK IN SHARE MODE". + // PostgreSQL and MySQL 8.0+: "FOR SHARE". // Returns "" for dialects that do not support row-level locking (e.g. SQLite). // A non-empty value is only returned when SupportsForUpdate() is true. ForShareClause() string // SupportsForShareOf reports whether the dialect supports an OF table list - // on the shared-lock clause (FOR SHARE … OF / LOCK IN SHARE MODE … OF). - // True for PostgreSQL (FOR SHARE OF …); false for MySQL (LOCK IN SHARE MODE - // does not accept an OF clause) and SQLite (no row-level locking at all). + // on the shared-lock clause (FOR SHARE … OF). True for PostgreSQL; false for + // MySQL (Grizzle does not expose MySQL's table-list lock option) and SQLite + // (no row-level locking at all). SupportsForShareOf() bool // SupportsRegexpMatch reports whether the dialect supports PostgreSQL-style @@ -175,6 +179,8 @@ func (postgresDialect) SupportsRegexpMatch() bool { return true } func (postgresDialect) SupportsFullTextSearch() bool { return true } func (postgresDialect) SupportsLimitOnMutate() bool { return false } +func (postgresDialect) SupportsOnConflictConstraint() bool { return true } + func (postgresDialect) Placeholder(n int) string { return fmt.Sprintf("$%d", n) } @@ -185,10 +191,10 @@ func (postgresDialect) QuoteIdent(name string) string { } // ------------------------------------------------------------------- -// MySQL / MariaDB +// MySQL 8.0+ // ------------------------------------------------------------------- -// MySQLDialect generates MySQL-compatible SQL. +// MySQLDialect generates MySQL 8.0+-compatible SQL. var MySQL Dialect = mysqlDialect{} type mysqlDialect struct{} @@ -205,12 +211,14 @@ func (mysqlDialect) SupportsForUpdate() bool { return true } func (mysqlDialect) SupportsForNoKeyUpdate() bool { return false } func (mysqlDialect) SupportsFullJoin() bool { return false } func (mysqlDialect) SupportsRightJoin() bool { return true } -func (mysqlDialect) ForShareClause() string { return "LOCK IN SHARE MODE" } +func (mysqlDialect) ForShareClause() string { return "FOR SHARE" } func (mysqlDialect) SupportsForShareOf() bool { return false } func (mysqlDialect) SupportsRegexpMatch() bool { return false } func (mysqlDialect) SupportsFullTextSearch() bool { return false } func (mysqlDialect) SupportsLimitOnMutate() bool { return true } +func (mysqlDialect) SupportsOnConflictConstraint() bool { return false } + func (mysqlDialect) Placeholder(_ int) string { return "?" } func (mysqlDialect) QuoteIdent(name string) string { @@ -244,6 +252,8 @@ func (sqliteDialect) SupportsRegexpMatch() bool { return false } func (sqliteDialect) SupportsFullTextSearch() bool { return false } func (sqliteDialect) SupportsLimitOnMutate() bool { return true } +func (sqliteDialect) SupportsOnConflictConstraint() bool { return false } + func (sqliteDialect) Placeholder(_ int) string { return "?" } func (sqliteDialect) QuoteIdent(name string) string { diff --git a/dialect/dialect_test.go b/dialect/dialect_test.go index ea87399..bf5a4c4 100644 --- a/dialect/dialect_test.go +++ b/dialect/dialect_test.go @@ -25,6 +25,7 @@ func TestDialectFeatureMatrix(t *testing.T) { supportsRegexpMatch bool supportsFullTextSearch bool supportsIgnoreConflict bool + supportsConstraint bool } cases := []row{ @@ -44,6 +45,7 @@ func TestDialectFeatureMatrix(t *testing.T) { supportsRegexpMatch: true, supportsFullTextSearch: true, supportsIgnoreConflict: true, + supportsConstraint: true, }, { name: "mysql", @@ -57,10 +59,11 @@ func TestDialectFeatureMatrix(t *testing.T) { supportsRightJoin: true, supportsForShareOf: false, supportsLimitOnMutate: true, - forShareClause: "LOCK IN SHARE MODE", + forShareClause: "FOR SHARE", supportsRegexpMatch: false, supportsFullTextSearch: false, supportsIgnoreConflict: true, + supportsConstraint: false, }, { name: "sqlite", @@ -78,6 +81,7 @@ func TestDialectFeatureMatrix(t *testing.T) { supportsRegexpMatch: false, supportsFullTextSearch: false, supportsIgnoreConflict: true, + supportsConstraint: false, }, } @@ -108,6 +112,7 @@ func TestDialectFeatureMatrix(t *testing.T) { checkBool("SupportsRegexpMatch", c.d.SupportsRegexpMatch(), c.supportsRegexpMatch) checkBool("SupportsFullTextSearch", c.d.SupportsFullTextSearch(), c.supportsFullTextSearch) checkBool("SupportsIgnoreConflicts", c.d.SupportsIgnoreConflicts(), c.supportsIgnoreConflict) + checkBool("SupportsOnConflictConstraint", c.d.SupportsOnConflictConstraint(), c.supportsConstraint) }) } } diff --git a/docs/guide/mutations.md b/docs/guide/mutations.md index 26330be..8a336d1 100644 --- a/docs/guide/mutations.md +++ b/docs/guide/mutations.md @@ -91,9 +91,9 @@ query.InsertInto(db.UsersT). DoUpdateSetStruct(UserUpdate{Enabled: &enabled}) ``` -### Grizzle-only / future constraint targets +### Grizzle-only constraint targets -Drizzle RC.1 PostgreSQL conflict targets are column-based; SQLite also accepts trusted SQL conflict-target expressions. A named-constraint conflict helper such as `OnConflictConstraint("users_realm_username_idx")` is not RC.1 parity and must stay out of the initial parity path unless it is separately implemented and labeled as a Grizzle-only extension. +Drizzle RC.1 PostgreSQL conflict targets are column-based; SQLite also accepts trusted SQL conflict-target expressions. `OnConflictConstraint("users_realm_username_idx")` is a Grizzle-only PostgreSQL extension. SQLite does not support the `ON CONSTRAINT` target form, so building this helper with SQLite (or any dialect whose `SupportsOnConflictConstraint()` returns false) fails with `unsupported_feature`. ### Dialect-specific ignore helpers diff --git a/docs/index.md b/docs/index.md index ebe8f82..a14e44e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ features: - icon: 🗄️ title: Multi-dialect - details: Dialect-aware builders target PostgreSQL, MySQL/MariaDB, and SQLite. Shared SQL stays portable; dialect-specific mutation APIs handle differences like MySQL duplicate-key updates. + details: Dialect-aware builders target PostgreSQL, MySQL 8.0+, and SQLite. Shared SQL stays portable; dialect-specific mutation APIs handle differences like MySQL duplicate-key updates. - icon: 🔧 title: Migration kit diff --git a/docs/reference/dialects.md b/docs/reference/dialects.md index 82a9946..d4bdbee 100644 --- a/docs/reference/dialects.md +++ b/docs/reference/dialects.md @@ -11,10 +11,14 @@ Query builds fail fast when a requested feature is unsupported and return import "github.com/sofired/grizzle/dialect" dialect.Postgres // PostgreSQL-compatible SQL; CockroachDB needs dedicated validation before initial support -dialect.MySQL // MySQL / MariaDB +dialect.MySQL // MySQL 8.0+ dialect.SQLite // SQLite 3.35+ baseline; RIGHT/FULL JOIN requires SQLite 3.39+ ``` +The built-in `dialect.MySQL` targets MySQL 8.0+ and renders modern shared locks +as `FOR SHARE`. It does not claim MariaDB compatibility; MariaDB requires a +separately validated custom dialect. + ## Comparison SQLite RIGHT/FULL OUTER JOIN support starts in SQLite 3.39.0. The built-in SQLite dialect targets a 3.35+ baseline and therefore returns false from `SupportsRightJoin()` and `SupportsFullJoin()`; a version-aware custom dialect may return true for engines known to be 3.39+. See the [SQLite 3.39.0 release notes](https://www.sqlite.org/releaselog/3_39_0.html). @@ -26,6 +30,7 @@ SQLite RIGHT/FULL OUTER JOIN support starts in SQLite 3.39.0. The built-in SQLit | Normal `RETURNING` clause | Yes | No | Yes (3.35+) | | Insert ID return | normal `RETURNING` | `.ReturningID()` parity for Drizzle `$returningId()` | normal `RETURNING` | | Upsert style | `ON CONFLICT … DO UPDATE` | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT … DO UPDATE` | +| Named-constraint conflict target | `ON CONFLICT ON CONSTRAINT …` | No API or fail-fast | No API or fail-fast | | Insert ignore / do-nothing conflict | `ON CONFLICT … DO NOTHING` | `INSERT IGNORE` | `ON CONFLICT … DO NOTHING` | | Non-recursive SELECT CTEs (`With` / `CTERef`) | Yes; Go API shape is DEVIATION:LANGUAGE | Yes (8.0+); Go API shape is DEVIATION:LANGUAGE | Yes (3.8.3+); Go API shape is DEVIATION:LANGUAGE | | Mutation CTE builders | DEVIATION:GAP (not designed) for insert/update/delete CTE APIs | DEVIATION:GAP (not designed) for update/delete CTE APIs; reviewed RC.1 MySQL insert path does not expose `withList` | DEVIATION:GAP (not designed) for insert/update/delete CTE APIs | @@ -84,6 +89,7 @@ type Dialect interface { // UpsertStyle returns the conflict-resolution style. UpsertStyle() UpsertStyle + SupportsOnConflictConstraint() bool // Dialect-specific INSERT keyword for ignore-conflict syntax. InsertIgnoreClause() string @@ -108,6 +114,10 @@ type Dialect interface { The shared `IgnoreConflicts()` helper is enabled only when `SupportsIgnoreConflicts()` is true. `UpsertStyle()` and `InsertIgnoreClause()` then select the classified syntax without branching on `Name()`. +The Grizzle-only `OnConflictConstraint()` helper is enabled only when +`SupportsOnConflictConstraint()` is true. PostgreSQL returns true; the built-in +MySQL and SQLite dialects return false and fail closed with `unsupported_feature`. + ## Implementing a custom dialect Any type that satisfies the `Dialect` interface can be used. For example, a PostgreSQL-compatible custom dialect can override identifier quoting. This is illustrative only; dedicated CockroachDB support remains outside the initial Grizzle scope until explicitly specified and tested. @@ -122,6 +132,7 @@ func (CRDBDialect) QuoteIdent(name string) string { } func (CRDBDialect) SupportsReturning() bool { return true } func (CRDBDialect) UpsertStyle() dialect.UpsertStyle { return dialect.UpsertOnConflict } +func (CRDBDialect) SupportsOnConflictConstraint() bool { return false } // fail closed until validated func (CRDBDialect) InsertIgnoreClause() string { return "" } func (CRDBDialect) SupportsIgnoreConflicts() bool { return false } func (CRDBDialect) SupportsCTE() bool { return true } diff --git a/docs/spec/dialects.md b/docs/spec/dialects.md index dcf09e5..624acc7 100644 --- a/docs/spec/dialects.md +++ b/docs/spec/dialects.md @@ -7,7 +7,7 @@ Grizzle's dialect system is the Go equivalent of [Drizzle's multi-dialect suppor | Dialect | Drizzle package | Grizzle package | Status | |---|---|---|---| | PostgreSQL | `drizzle-orm/pg-core` | `schema/pg`, `dialect.Postgres` | PARITY target with listed gaps; required initial scope | -| MySQL / MariaDB | `drizzle-orm/mysql-core` | `schema/mysql`, `dialect.MySQL` | DEVIATION:GAP (designed) for remaining column type gaps; see [schema.md](./schema.md) | +| MySQL 8.0+ | `drizzle-orm/mysql-core` | `schema/mysql`, `dialect.MySQL` | DEVIATION:GAP (designed) for remaining column type gaps; see [schema.md](./schema.md) | | SQLite | `drizzle-orm/sqlite-core` | `schema/sqlite`, `dialect.SQLite` | DEVIATION:GAP (designed) for remaining column type gaps; see [schema.md](./schema.md) | | CockroachDB | `drizzle-orm/cockroach-core` | Use `dialect.Postgres` where compatible only after dedicated validation | DEVIATION:GAP (not designed); file-migration support is out of initial scope | | Neon, Supabase | Use `pg-core` | Use `dialect.Postgres` where driver-compatible | DEVIATION:GAP (not designed); file-migration support depends on driver capability | @@ -60,6 +60,7 @@ type Dialect interface { Name() string SupportsReturning() bool UpsertStyle() UpsertStyle + SupportsOnConflictConstraint() bool // PostgreSQL ON CONFLICT ON CONSTRAINT only MySQLInsertIgnoreKeyword() string SQLiteOnConflictDoNothingClause() string SupportsCTE() bool diff --git a/docs/spec/query-builder.md b/docs/spec/query-builder.md index 781e5db..4175e4e 100644 --- a/docs/spec/query-builder.md +++ b/docs/spec/query-builder.md @@ -720,7 +720,7 @@ PostgreSQL and SQLite: | `.onConflictDoUpdate({target, set})` | `.OnConflict(query.ConflictColumn(col), ...).DoUpdateSetExcluded(cols...)` | PARITY | | PostgreSQL conflict target as columns | `query.ConflictColumn(col)` | PARITY | | SQLite conflict target as columns or trusted SQL expression | `query.ConflictColumn(col)` or `query.SQLiteConflictExpr(expr)` | PARITY target for both target shapes; `SQLiteConflictExpr` is DEVIATION:GAP (designed) until implemented | -| Constraint-name conflict target | `.OnConflictConstraint(name)` | GRIZZLE-ONLY / future extension; not RC.1 parity | +| Constraint-name conflict target | `.OnConflictConstraint(name)` | GRIZZLE-ONLY; PostgreSQL only, gated by `SupportsOnConflictConstraint()` | | `set` with `excluded` reference | `.DoUpdateSetExcluded(cols...)` | PARITY | | `set` with arbitrary value | `query.SetValue(columnHandle, val)` passed to `.DoUpdateSet(...)` | PARITY | | `set` with expression | `query.SetExpr(columnHandle, expr)` passed to `.DoUpdateSet(...)` | PARITY | diff --git a/query/insert.go b/query/insert.go index 33a17c8..7ed0132 100644 --- a/query/insert.go +++ b/query/insert.go @@ -129,6 +129,8 @@ func (b *InsertBuilder) OnConflict(cols ...string) *InsertBuilder { } // OnConflictConstraint sets the conflict target to a named constraint. +// Dialects without ON CONFLICT ON CONSTRAINT support cause Build to return +// ErrUnsupportedFeature. // // query.InsertInto(UsersT).Values(row). // OnConflictConstraint("users_realm_username_idx").DoNothing() @@ -248,6 +250,9 @@ func (b *InsertBuilder) Build(d dialect.Dialect) (string, []any, error) { if b.ignoreConflict && b.upsert != nil { return buildFailure("build_insert", NewError(CodeBuildValidation, "build_insert", "ignore conflicts cannot be combined with an upsert clause")) } + if b.upsert != nil && b.upsert.conflictConstraint != "" && !d.SupportsOnConflictConstraint() { + return buildFailure("build_insert", NewError(CodeUnsupportedFeature, "build_insert", "named constraint conflict targets are not supported by this dialect")) + } var sb strings.Builder // Choose INSERT keyword based on ignore flag and dialect support. diff --git a/query/query_test.go b/query/query_test.go index 73e05a9..17a0eb6 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -428,6 +428,16 @@ func TestUpsert_OnConflictConstraint(t *testing.T) { ) } +func TestUpsert_OnConflictConstraint_SQLite_ReturnsUnsupportedFeature(t *testing.T) { + name := "test-realm" + row := ts.RealmInsert{Name: name} + q := query.InsertInto(ts.RealmsT). + Values(row). + OnConflictConstraint("realms_name_idx"). + DoNothing() + assertBuildError(t, q, dialect.SQLite, query.ErrUnsupportedFeature) +} + func TestUpsert_MultiColConflictTarget(t *testing.T) { realmID := uuid.MustParse("00000000-0000-0000-0000-000000000001") username := "alice" @@ -1757,9 +1767,34 @@ func TestSelect_ForShare_Postgres(t *testing.T) { func TestSelect_ForShare_MySQL(t *testing.T) { q := query.Select().From(ts.UsersT).ForShare() - got, _, _ := q.Build(dialect.MySQL) - if !strings.Contains(got, "LOCK IN SHARE MODE") { - t.Errorf("expected LOCK IN SHARE MODE in: %s", got) + got, _, err := q.Build(dialect.MySQL) + if err != nil { + t.Fatalf("Build() error: %v", err) + } + want := "SELECT * FROM `users` FOR SHARE" + if got != want { + t.Errorf("got: %s\nwant: %s", got, want) + } +} + +func TestSelect_ForShareOptions_MySQL(t *testing.T) { + for _, tc := range []struct { + name string + opt query.LockOption + want string + }{ + {name: "nowait", opt: query.NoWait, want: "SELECT * FROM `users` FOR SHARE NOWAIT"}, + {name: "skip locked", opt: query.SkipLocked, want: "SELECT * FROM `users` FOR SHARE SKIP LOCKED"}, + } { + t.Run(tc.name, func(t *testing.T) { + got, _, err := query.Select().From(ts.UsersT).For(query.LockForShare, tc.opt).Build(dialect.MySQL) + if err != nil { + t.Fatalf("Build() error: %v", err) + } + if got != tc.want { + t.Errorf("got: %s\nwant: %s", got, tc.want) + } + }) } } @@ -2637,20 +2672,21 @@ func (noCTEDialect) SupportsReturning() bool { return false } func (noCTEDialect) UpsertStyle() dialect.UpsertStyle { return dialect.UpsertOnConflict } -func (noCTEDialect) InsertIgnoreClause() string { return "" } -func (noCTEDialect) SupportsIgnoreConflicts() bool { return false } -func (noCTEDialect) SupportsCTE() bool { return false } -func (noCTEDialect) SupportsWindowFunctions() bool { return true } -func (noCTEDialect) SupportsDistinctOn() bool { return false } -func (noCTEDialect) SupportsForUpdate() bool { return false } -func (noCTEDialect) SupportsForNoKeyUpdate() bool { return false } -func (noCTEDialect) SupportsForShareOf() bool { return false } -func (noCTEDialect) SupportsFullJoin() bool { return false } -func (noCTEDialect) SupportsRightJoin() bool { return false } -func (noCTEDialect) ForShareClause() string { return "" } -func (noCTEDialect) SupportsRegexpMatch() bool { return false } -func (noCTEDialect) SupportsFullTextSearch() bool { return false } -func (noCTEDialect) SupportsLimitOnMutate() bool { return false } +func (noCTEDialect) SupportsOnConflictConstraint() bool { return false } +func (noCTEDialect) InsertIgnoreClause() string { return "" } +func (noCTEDialect) SupportsIgnoreConflicts() bool { return false } +func (noCTEDialect) SupportsCTE() bool { return false } +func (noCTEDialect) SupportsWindowFunctions() bool { return true } +func (noCTEDialect) SupportsDistinctOn() bool { return false } +func (noCTEDialect) SupportsForUpdate() bool { return false } +func (noCTEDialect) SupportsForNoKeyUpdate() bool { return false } +func (noCTEDialect) SupportsForShareOf() bool { return false } +func (noCTEDialect) SupportsFullJoin() bool { return false } +func (noCTEDialect) SupportsRightJoin() bool { return false } +func (noCTEDialect) ForShareClause() string { return "" } +func (noCTEDialect) SupportsRegexpMatch() bool { return false } +func (noCTEDialect) SupportsFullTextSearch() bool { return false } +func (noCTEDialect) SupportsLimitOnMutate() bool { return false } // noWindowDialect is a test-only dialect that reports SupportsWindowFunctions() = false. type noWindowDialect struct{ noCTEDialect } diff --git a/query/select.go b/query/select.go index 031d8c4..e714ec6 100644 --- a/query/select.go +++ b/query/select.go @@ -135,8 +135,8 @@ func (b *SelectBuilder) ForUpdate() *SelectBuilder { return b.For(LockForUpdate) } -// ForShare appends FOR SHARE (PostgreSQL) / LOCK IN SHARE MODE (MySQL) to -// the query, locking rows for read while allowing other readers. +// ForShare appends FOR SHARE to the query, locking rows for read while allowing +// other readers. // PostgreSQL and MySQL only; unsupported dialects cause Build to return // ErrUnsupportedFeature. // @@ -703,8 +703,8 @@ func (b *SelectBuilder) buildWith(ctx *expr.BuildContext) (string, error) { case LockForShare: sb.WriteString(" " + ctx.Dialect().ForShareClause()) // OF table list: only emitted when the dialect declares support for it - // (e.g. PostgreSQL FOR SHARE). MySQL's LOCK IN SHARE MODE does not - // accept an OF clause, so SupportsForShareOf() returns false there. + // (e.g. PostgreSQL FOR SHARE). Grizzle does not expose this option for + // MySQL, so SupportsForShareOf() returns false there. if len(b.lockOf) > 0 && !ctx.Dialect().SupportsForShareOf() { return "", NewError(CodeUnsupportedFeature, "build_select", "row-lock table lists are not supported by this dialect") } From 35a06b35add481b9589b16573f55b0f0122e5fc2 Mon Sep 17 00:00:00 2001 From: Charles Fineman Date: Sun, 19 Jul 2026 20:03:54 +0000 Subject: [PATCH 2/2] docs(query): clarify dialect capability contract --- docs/spec/dialects.md | 30 ++++++++++++++---------------- query/select.go | 9 +++++---- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/docs/spec/dialects.md b/docs/spec/dialects.md index 624acc7..6fa9e7b 100644 --- a/docs/spec/dialects.md +++ b/docs/spec/dialects.md @@ -41,9 +41,8 @@ Window-function SQL capability is a database feature row, not a claim that Drizz All query builder operations must route dialect-specific SQL through the `Dialect` interface — no dialect name checks (`if d.Name() == "postgres"`) inside query builder code. -Target interface: - -The current branch still exposes older names such as `InsertIgnoreClause` and `SupportsForShareOf` in places. The interface below is the RC.1-parity target; implementation must either migrate the current interface to this shape or provide compatibility shims while preserving the target semantics. +Current interface (the authoritative definition is `dialect.Dialect` in +`dialect/dialect.go`): ```go type UpsertStyle string @@ -56,26 +55,25 @@ const ( type Dialect interface { Placeholder(n int) string - QuoteIdent(name string) (string, error) + QuoteIdent(name string) string Name() string SupportsReturning() bool UpsertStyle() UpsertStyle SupportsOnConflictConstraint() bool // PostgreSQL ON CONFLICT ON CONSTRAINT only - MySQLInsertIgnoreKeyword() string - SQLiteOnConflictDoNothingClause() string + InsertIgnoreClause() string + SupportsIgnoreConflicts() bool SupportsCTE() bool SupportsWindowFunctions() bool SupportsDistinctOn() bool - SupportsForUpdate() bool // FOR UPDATE / FOR SHARE - SupportsForNoKeyUpdate() bool // PostgreSQL-compatible; false for MySQL/SQLite - SupportsForKeyShare() bool // PostgreSQL-compatible; false for MySQL/SQLite - SupportsRightJoin() bool // true for PostgreSQL/MySQL; SQLite version/driver gated: true only for 3.39+ - SupportsFullJoin() bool // false for MySQL; SQLite version/driver gated: true only for 3.39+ - ForShareClause() string // "FOR SHARE" for PostgreSQL and MySQL RC.1 parity - SupportsLockOf() bool // PostgreSQL-compatible; absent from MySQL/SQLite in RC.1 - SupportsRegexpMatch() bool // PostgreSQL-only (~, ~*, !~, !~*); false for MySQL/SQLite - SupportsFullTextSearch() bool // PostgreSQL-only (@@, to_tsvector, etc.); false for MySQL/SQLite - SupportsLimitOnMutate() bool // false for PostgreSQL; true for MySQL; SQLite must be driver/compile-option gated + SupportsForUpdate() bool + SupportsForNoKeyUpdate() bool + SupportsFullJoin() bool + SupportsRightJoin() bool + ForShareClause() string + SupportsForShareOf() bool + SupportsRegexpMatch() bool + SupportsFullTextSearch() bool + SupportsLimitOnMutate() bool } ``` diff --git a/query/select.go b/query/select.go index e714ec6..bc9f740 100644 --- a/query/select.go +++ b/query/select.go @@ -135,10 +135,11 @@ func (b *SelectBuilder) ForUpdate() *SelectBuilder { return b.For(LockForUpdate) } -// ForShare appends FOR SHARE to the query, locking rows for read while allowing -// other readers. -// PostgreSQL and MySQL only; unsupported dialects cause Build to return -// ErrUnsupportedFeature. +// ForShare appends the dialect's shared row-lock clause to the query, locking +// rows for read while allowing other readers. +// The built-in PostgreSQL and MySQL dialects support it; custom dialects may +// opt in through their row-locking capabilities. Unsupported dialects cause +// Build to return ErrUnsupportedFeature. // // ForShare is a convenience wrapper around For(LockForShare). func (b *SelectBuilder) ForShare() *SelectBuilder {