Documentation
¶
Overview ¶
Package dialect names the SQL dialects the module's SQL-emitting packages support, and carries the small helpers every one of them otherwise reimplements: bind-marker rendering, identifier vetting, and DDL statement splitting.
It exists to be a leaf. database/migrate, outbox, and authorization/database all speak the same three dialects, and their migrations subpackages cannot import their parents without closing a cycle through the parents' tests — so before this package, each of the five declared its own Dialect type and tests converted between them. One shared type makes those conversions unrepresentable.
Index ¶
- Constants
- Variables
- func RequireDialect(component string, d Dialect, want ...Dialect) error
- func RequirePostgres(component string, d Dialect) error
- func SplitStatements(ddl string) []string
- func ValidIdentifier(s string) bool
- type Dialect
- func (d Dialect) Placeholder(n int) string
- func (d Dialect) Placeholders(start, count int) string
- func (d Dialect) QuoteIdentifier(id string) string
- func (d Dialect) SupportsNotify() bool
- func (d Dialect) SupportsSkipLocked() bool
- func (d Dialect) SupportsWriteLimit() bool
- func (d Dialect) Valid() bool
Constants ¶
const PostgresNotifyStatement = `SELECT pg_notify($1, '')`
PostgresNotifyStatement emits a payload-free notification on the channel bound to it, waking anything listening on that channel — see database/postgres/pgnotify for the other end.
The payload is empty on purpose. Postgres collapses duplicate (channel, payload) pairs within a transaction, so a transaction that notifies fifty times sends one notification, and there is nothing in it for a consumer to come to depend on. The channel is bound rather than interpolated; the listening side has to render it into a LISTEN, which takes no parameters, so it is vetted with ValidIdentifier there.
It lives here, in the leaf every SQL-emitting package already imports, rather than beside the listener: outbox serves three dialects and workqueue serves one, and neither should take a pgx dependency to reach a constant.
Variables ¶
var ( IdentifierChars = charset.ASCIIAlphanumeric.Union(charset.Bytes('_')) IdentifierLeadChars = charset.ASCIILetters.Union(charset.Bytes('_')) )
IdentifierChars and IdentifierLeadChars are the alphabet a SQL identifier is drawn from, and the narrower one its first character comes from. A leading digit is excluded so a bare name is never mistakable for a number.
They are exported so that the rules built on top of an identifier — a table prefix, which is an identifier fragment or nothing — can be assembled from the same alphabet rather than from a second copy of it that could drift.
ASCII only. Admitting the full Unicode letter category would let two names that render identically — homoglyphs, or the same string in NFC and NFD — claim to be two different tables.
var ErrInvalidIdentifier = platformerrors.New("invalid SQL identifier")
ErrInvalidIdentifier indicates a name that ValidIdentifier rejects. Packages wrap it with their own context, so errors.Is works across all of them — including across a package that builds a table's DDL and one that queries it, which is the pair most likely to be checked against each other.
var ErrUnsupported = platformerrors.New("unsupported SQL dialect")
ErrUnsupported indicates a dialect outside the supported set. Packages wrap it with their own context, so errors.Is works across all of them.
Functions ¶
func RequireDialect ¶
RequireDialect returns a wrapped ErrUnsupported naming component and d unless d is one of want.
It is for the packages whose SQL is written against particular dialects rather than reduced to a portable subset, so that all of them refuse the same way and at the same moment — construction — instead of emitting syntax the server rejects on the first query. component names the caller in the message ("work queue", "workqueue migration"), since a process wiring several of these needs to know which one objected.
want is variadic because the constraint is not always a single dialect: this module holds packages that support Postgres and MySQL but not SQLite, which has no SKIP LOCKED. Prefer Valid over listing all three.
Calling it with no accepted dialects is a programming error rather than a vacuous pass, and says so.
func RequirePostgres ¶
RequirePostgres is RequireDialect for the Postgres-only packages, which are the common case — the work queue and its migrations both reach for it.
func SplitStatements ¶
SplitStatements strips '--' comments from ddl and splits it into individually executable statements on ';', preserving statement order.
Comments come out before the split, not after. A '--' comment may contain a semicolon — prose routinely does — and splitting first tears such a comment in half, leaving its tail masquerading as SQL at the head of the next statement.
Comment stripping handles whole-line '--' comments and blank lines only, not a '--' appearing after SQL on the same line, nor semicolons inside string literals; the DDL shipped by this module contains neither, and the round-trip tests against real servers are what keep that true.
func ValidIdentifier ¶
ValidIdentifier reports whether s is safe to interpolate into query text as a table name. Table names are interpolated rather than bound, so they are restricted rather than escaped.
Types ¶
type Dialect ¶
type Dialect string
Dialect selects the SQL a package emits. It must match the database provider the emitted SQL runs against.
const ( // Postgres targets PostgreSQL, which numbers its placeholders and supports // SKIP LOCKED. Postgres Dialect = "postgres" // MySQL targets MySQL 8.0+ — the first version with WITH RECURSIVE — which // supports SKIP LOCKED. MySQL Dialect = "mysql" // SQLite targets SQLite, which is single-writer by nature and has no // SKIP LOCKED. SQLite Dialect = "sqlite" )
func (Dialect) Placeholder ¶
Placeholder renders the n-th bind marker (1-indexed). Postgres numbers its placeholders; MySQL and SQLite do not.
func (Dialect) Placeholders ¶
Placeholders renders count bind markers starting at start, joined for use inside an IN clause or a VALUES tuple.
func (Dialect) QuoteIdentifier ¶
QuoteIdentifier renders an identifier as a quoted one for d, doubling any embedded quote character so it cannot end the quoting early.
It is the escaping counterpart to ValidIdentifier's restricting, and both exist because a table or column name is interpolated into statement text rather than bound. Prefer ValidIdentifier where the name comes from configuration and a rejection is actionable; this is for the names that are legal-but-awkward — a mixed-case column, a reserved word — where refusing would be refusing a database somebody already has.
Postgres and SQLite quote with double-quotes per the SQL standard; MySQL quotes with backticks. An unrecognized dialect gets the standard form, which is the one every dialect here but MySQL uses.
This is not sanitization for arbitrary input. A NUL byte, or a name from a hostile source, still belongs in ValidIdentifier's hands: doubling the quote character makes a legal identifier safe to quote, not an arbitrary string safe to interpolate.
func (Dialect) SupportsNotify ¶
SupportsNotify reports whether the dialect can signal a listening session with LISTEN/NOTIFY, which is what lets a poller be woken instead of waiting out its interval.
func (Dialect) SupportsSkipLocked ¶
SupportsSkipLocked reports whether the dialect can claim rows with FOR UPDATE SKIP LOCKED, which is what allows more than one competing worker to claim from the same table at once.
func (Dialect) SupportsWriteLimit ¶
SupportsWriteLimit reports whether the dialect caps a DELETE or an UPDATE with that statement's own ORDER BY and LIMIT.
It is what decides the shape of a bounded write, which every reaper in this module renders one of. MySQL takes the cap on the write itself. Postgres does not parse it at all, and SQLite parses it only in builds compiled with SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which most are not — so that spelling there is a failure waiting for run time rather than one a generator would see. Both of those bound a read instead and write away whatever it named.
What it does not say is that MySQL has no read-bounded form. It refuses only the unmaterialized one, where the read scans the table being written (ER_UPDATE_TABLE_USED, error 1093), and accepts the identical rows through a derived table. So this is the answer to "may the write carry its own bound", and not to "is there any other way to bound it" — see database/querygen's boundedWriteForm, which composes this into the full set of spellings a server accepts.
It lives here for PostgresNotifyStatement's reason: two packages depend on it, one of them renders a corpus and the other composes SQL for a driver, and neither should be learning a server's grammar from the other. A second copy of this answer is one that can drift, and the copy retention carried had — it read MySQL's refusal as covering every read-bounded form.