Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
32 changes: 21 additions & 11 deletions dialect/dialect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
scottescue marked this conversation as resolved.

// 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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{}
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion dialect/dialect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func TestDialectFeatureMatrix(t *testing.T) {
supportsRegexpMatch bool
supportsFullTextSearch bool
supportsIgnoreConflict bool
supportsConstraint bool
}

cases := []row{
Expand All @@ -44,6 +45,7 @@ func TestDialectFeatureMatrix(t *testing.T) {
supportsRegexpMatch: true,
supportsFullTextSearch: true,
supportsIgnoreConflict: true,
supportsConstraint: true,
},
{
name: "mysql",
Expand All @@ -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",
Expand All @@ -78,6 +81,7 @@ func TestDialectFeatureMatrix(t *testing.T) {
supportsRegexpMatch: false,
supportsFullTextSearch: false,
supportsIgnoreConflict: true,
supportsConstraint: false,
},
}

Expand Down Expand Up @@ -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)
})
}
}
4 changes: 2 additions & 2 deletions docs/guide/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion docs/reference/dialects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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 |
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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 }
Expand Down
33 changes: 16 additions & 17 deletions docs/spec/dialects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand All @@ -56,25 +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
MySQLInsertIgnoreKeyword() string
SQLiteOnConflictDoNothingClause() string
SupportsOnConflictConstraint() bool // PostgreSQL ON CONFLICT ON CONSTRAINT only
InsertIgnoreClause() string
SupportsIgnoreConflicts() bool
SupportsCTE() bool
Comment on lines 61 to 65

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 35a06b3. I replaced the mixed aspirational/current snippet with the exact current dialect.Dialect interface, corrected every method name/signature, and pointed readers to dialect/dialect.go as authoritative. The aspirational gating rules remain separately labeled below.

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
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/spec/query-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
5 changes: 5 additions & 0 deletions query/insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down
Loading