feat(lagoon): per-query collation for OrderBy, drop the database locale check
- lagoon.OrderBy takes variadic lagoon.OrderOption values; lagoon.Collate(name) emits a validated, double-quoted COLLATE clause (e.g. "pl-x-icu") - remove the exported CheckLocale and the ICU pl-PL check from Open and Use - framework test containers and per-test databases are plain PostgreSQL - lagoon README, root README and docs pages drop the locale requirement; queries-and-pagination gains a "Sorting with a collation" section backed by ExampleCollate
This commit is contained in:
@@ -15,11 +15,10 @@ Postgres data layer: the shared GORM connection, per-plugin migrations, model he
|
||||
- One shared pool: `lagoon.Open`, `lagoon.Use` and `lagoon.OpenFromApp` return a `*sql.DB` and a `*gorm.DB` built on that same pool; `lagoon.Publish` makes both available on the `backpack.App`.
|
||||
- Database-ready hooks: `lagoon.OnDatabase` runs a callback with the pool and GORM handle as soon as the database is published, immediately when it already is, otherwise when `lagoon.Publish` runs. Plugins register GORM callbacks through it from Boot, which runs before the `serve` command publishes the database.
|
||||
- After-commit work: `lagoon.Transaction` runs a function in a transaction and then the callbacks registered with `lagoon.AfterCommit`, in order, only after the commit succeeds; a nested `lagoon.Transaction` is a savepoint whose callbacks are dropped with it when it fails. A nested `lagoon.Transaction` must be given the outer transaction's handle: given a root handle it returns an error without running its function, rather than open an independent transaction whose callbacks would wait on the outer one. A single-statement write for which GORM opens its own implicit transaction runs its callbacks from `lagoon:after_commit` once GORM commits, and never when the write fails. A callback registered inside a foreign plain GORM transaction is unsafe because Lagoon cannot observe its commit, so `lagoon.AfterCommit` warns and skips it. Outside a transaction, callbacks run immediately. The handle a supported callback receives always has an empty statement on the connection its work belongs to. A panicking callback is logged and never turns a committed write into an error.
|
||||
- Database check at connect time: `lagoon.CheckLocale` refuses a database whose default collation is not the ICU `pl-PL` locale, so ordering matches the database default without per-query `COLLATE`.
|
||||
- Per-plugin migrations: `lagoon.Migrate` runs the framework's `system_files` set (`attach.Migrations`), backend admin identity set (`lagoon.BackendAdminMigrations`) and job-queue set (`lagoon.QueueMigrations`: River's schema pinned at `lagoon.RiverSchemaVersion`, then the `lagoon.JobsTable` record table, under the `lagoon.QueueHistoryID` history), then every `pact.HasMigrations` set in plugin activation order, each in its own `summer_migrations_<plugin_id>` history table (`lagoon.HistoryTableName`). `lagoon.RollbackLast` and `lagoon.Status` cover rollback and history.
|
||||
- Mass assignment: `lagoon.Fill` copies only allow-listed keys onto a model by GORM column name and silently drops the rest, logging each dropped key once outside production. A `json.Number` (from a decoder using `UseNumber`) fills integer, unsigned and float fields. A value that does not fit its column (a fraction, an exponent or an overflow for an integer field, or a value of the wrong type) is a `lagoon.FillTypeError` naming the key, so a caller can answer it as a validation failure on that field. `lagoon.HasFillable` and `lagoon.HasHidden` are the Go forms of `$fillable` and `$hidden`.
|
||||
- Validation: `lagoon.Validate` accepts Laravel-style rule strings (`required`, `nullable`, `integer`, `numeric`, `between`, `min`, `max`, `in`, `unique`, `boolean`, `email`, `confirmed`, `different`, `mimes`) and returns a field-to-messages map, translated through phrasebook when a translator is given. Unknown rule tokens are an error.
|
||||
- Safe ordering: `lagoon.OrderBy` appends an ORDER BY only for an allow-listed column and an `asc` or `desc` direction.
|
||||
- Safe ordering: `lagoon.OrderBy` appends an ORDER BY only for an allow-listed column and an `asc` or `desc` direction, and `lagoon.Collate` adds a validated `COLLATE` clause for language-specific text order (for example the ICU collation `pl-x-icu`); lagoon puts no requirement on the database's default locale.
|
||||
- Pagination: `lagoon.Paginate` builds a `lagoon.Page` with `data` and `meta` (`current_page`, `last_page`, `per_page`, `total`).
|
||||
- Column types: `lagoon.Encrypted` stores AES-256-GCM ciphertext under a key derived from `app.key`, decrypts with previous keys during rotation, and always redacts itself in JSON and string output; `lagoon.Jsonable` stores JSON as TEXT and keeps SQL NULL distinct from an empty value.
|
||||
- Lifecycle and relations: hook interfaces matching GORM's native method names (`lagoon.HasBeforeCreate`, `lagoon.HasBeforeSave`, `lagoon.HasBeforeDelete`, `lagoon.HasAfterDelete`) plus `lagoon.HasBeforeValidate`; `lagoon.WithSoftDeleteCascade` runs a cascade inside the parent delete; `lagoon.RegisterJoinTable` wires pivot models with business columns.
|
||||
@@ -110,15 +109,14 @@ func (p *Plugin) Migrations() []*gormigrate.Migration {
|
||||
| Identifier | Description |
|
||||
|------------|-------------|
|
||||
| `lagoon.OpenFromApp` | Opens the shared pool from `database.dsn` and publishes the `app.key` encryption keys. |
|
||||
| `lagoon.Open` | Opens and pings a DSN, checks the locale and returns the pool plus a GORM handle on it. |
|
||||
| `lagoon.Use` | Returns a GORM handle on an existing pool after the same checks. |
|
||||
| `lagoon.Open` | Opens and pings a DSN and returns the pool plus a GORM handle on it. |
|
||||
| `lagoon.Use` | Returns a GORM handle on an existing pool after a ping. |
|
||||
| `lagoon.Publish` | Stores the pool and GORM handle on the `backpack.App`, then runs the callbacks queued by `lagoon.OnDatabase`. |
|
||||
| `lagoon.OnDatabase` | Runs a callback with the pool and GORM handle once the database is published. |
|
||||
| `lagoon.Transaction` | Runs a function in a transaction (a savepoint when nested) and its `lagoon.AfterCommit` callbacks after the commit. |
|
||||
| `lagoon.AfterCommit` | Registers work to run after the surrounding transaction commits. |
|
||||
| `lagoon.AfterCommitCallback` | Name of the GORM callback, `lagoon:after_commit`, that runs single-statement after-commit work. |
|
||||
| `lagoon.DSN` | Reads `database.dsn` from config. |
|
||||
| `lagoon.CheckLocale` | Fails unless the database default locale is ICU `pl-PL`. |
|
||||
| `lagoon.Migrate` | Runs framework and plugin migrations in order. |
|
||||
| `lagoon.RollbackLast` | Rolls back the last migration of one plugin. |
|
||||
| `lagoon.Status` | Lists applied migration IDs per plugin as `lagoon.StatusRow` values. |
|
||||
@@ -137,7 +135,9 @@ func (p *Plugin) Migrations() []*gormigrate.Migration {
|
||||
| `lagoon.Fill` | Allow-listed mass assignment by column name. |
|
||||
| `lagoon.FillTypeError` | Returned by `lagoon.Fill` when a requested value does not fit its column; `Key` names the column. |
|
||||
| `lagoon.Validate` | Laravel-style rule validation with a `unique` database check. |
|
||||
| `lagoon.OrderBy` | Allow-listed ORDER BY. |
|
||||
| `lagoon.OrderBy` | Allow-listed ORDER BY, with an optional `lagoon.Collate`. |
|
||||
| `lagoon.Collate` | Order option that sorts the column with a named PostgreSQL collation; the name is validated and quoted. |
|
||||
| `lagoon.OrderOption` | Option type accepted by `lagoon.OrderBy`. |
|
||||
| `lagoon.Paginate` | Builds a `lagoon.Page` with `lagoon.PageMeta`. |
|
||||
| `lagoon.WithSoftDeleteCascade` | Runs a cascade inside the parent delete transaction. |
|
||||
| `lagoon.RegisterJoinTable` | Registers a custom pivot model for a many-to-many field. |
|
||||
@@ -198,4 +198,4 @@ storage:
|
||||
go test ./modules/lagoon/...
|
||||
```
|
||||
|
||||
The database tests in `lagoon` and `lagoon/attach` start a `postgres:16-alpine` container, initialised with the ICU `pl-PL` locale, through testcontainers-go, so they need a running Docker daemon. They are skipped by `go test -short ./modules/lagoon/...`, which runs only the unit tests.
|
||||
The database tests in `lagoon` and `lagoon/attach` start a `postgres:16-alpine` container through testcontainers-go, so they need a running Docker daemon. They are skipped by `go test -short ./modules/lagoon/...`, which runs only the unit tests.
|
||||
|
||||
Reference in New Issue
Block a user