auto: sync OpenClaw config 2026-07-28 14:00

This commit is contained in:
2026-07-28 14:00:29 +08:00
parent f3c5e82269
commit 0f85da3245
32 changed files with 4142 additions and 11 deletions
+163
View File
@@ -0,0 +1,163 @@
# Dialects — Where Engines Actually Differ
Portable SQL is a smaller language than most people write. This file is the list of places a statement that runs on one engine does something else on another without complaining, plus how to choose an engine in the first place.
Contents: Choosing an Engine · Identifiers and Quoting · Strings and Collation · NULL Ordering · Types · Auto-Increment · Upsert · Limit and Top · Returning · Grouping Rules · Window and CTE Support · DDL Transactionality · Booleans · Concatenation and Math · Error Behavior · Feature Floors · Portability Strategy
## Choosing an Engine
| Engine | Choose when | Real cost |
|---|---|---|
| SQLite | Embedded, single-machine, local-first apps, tests, CLI tools, read-heavy sites on one box | One writer at a time; no network access; type affinity instead of enforcement |
| PostgreSQL | Default for anything server-side: richest types, strictest correctness, extensions | Connection-per-process (needs pooling); more knobs to get wrong |
| MySQL / MariaDB | The platform or host dictates it, or the team's operational muscle is there | No transactional DDL; historically lenient defaults; MariaDB and MySQL have diverged |
| SQL Server | .NET/Windows shops, existing licensing, strong tooling requirements | Licensing; default lock-based isolation until `READ_COMMITTED_SNAPSHOT` is enabled |
Do not switch engines for a performance problem that is really a missing index or a bad plan. Do switch when the workload shape is wrong: heavy analytical scans belong in a columnar store (`duckdb`, `clickhouse`, a warehouse), not in a tuned OLTP database.
## Identifiers and Quoting
| Engine | Quote char | Unquoted case | Consequence |
|---|---|---|---|
| PostgreSQL | `"col"` | Folded to **lower**case | `CREATE TABLE "Users"` must be quoted forever after |
| MySQL | `` `col` `` | Preserved; table-name case sensitivity depends on the filesystem | A schema built on macOS breaks on Linux |
| SQLite | `"col"`, `` `col` ``, `[col]` | Preserved, compared case-insensitively | Very permissive; hides problems until you migrate |
| SQL Server | `[col]` | Preserved, compared by database collation | Usually case-insensitive |
Use lowercase snake_case unquoted everywhere and the problem never appears. MySQL's `lower_case_table_names` is set at initialization and cannot be safely changed afterwards — decide before the first deploy.
## Strings and Collation
- Default comparison: PostgreSQL and SQLite are case-**sensitive**; MySQL (`utf8mb4_0900_ai_ci`) and SQL Server are case-**insensitive** by default. The same `WHERE email = ?` finds different rows across engines.
- PostgreSQL: use `citext` or compare `lower(col)` with a matching expression index. MySQL: choose a `_bin` or `_cs` collation on the column when you need sensitivity.
- MySQL `utf8` is 3-byte and cannot store emoji or some CJK characters. `utf8mb4` is the only correct choice, and it must be set on the column, the table, and the connection (SKILL.md rule 7).
- Mixing collations across a join disables the index and can raise "illegal mix of collations" — align collations when creating tables, not later.
- `CHAR(n)` pads with spaces and some engines ignore trailing spaces in comparison; `VARCHAR`/`TEXT` do not. Avoid `CHAR` except for genuinely fixed-width codes.
- Empty string versus NULL: Oracle treats `''` as NULL; every engine here treats them as different values. Anything importing from Oracle inherits that ambiguity.
- Concatenating anything with NULL yields NULL in PostgreSQL, SQLite, and MySQL; SQL Server's `+` does too unless `CONCAT_NULL_YIELDS_NULL` is off, while `CONCAT()` ignores NULLs everywhere.
## NULL Ordering
| Engine | `ORDER BY col ASC` | Override |
|---|---|---|
| PostgreSQL | NULLs **last** | `NULLS FIRST` / `NULLS LAST` |
| SQLite | NULLs first | `NULLS LAST` (>=3.30) |
| MySQL | NULLs first | No clause — sort by `col IS NULL, col` |
| SQL Server | NULLs first | No clause — sort by `CASE WHEN col IS NULL THEN 1 ELSE 0 END, col` |
Adding an explicit `NULLS LAST` can disable an index that does not carry that ordering, so prefer making the column `NOT NULL` when the ordering matters.
## Types
| Concept | PostgreSQL | MySQL | SQLite | SQL Server |
|---|---|---|---|---|
| Unbounded text | `TEXT` | `TEXT`/`LONGTEXT` (cannot be fully indexed; needs a prefix length) | `TEXT` | `NVARCHAR(MAX)` |
| Exact decimal | `NUMERIC(p,s)` | `DECIMAL(p,s)` | `NUMERIC` (affinity only) | `DECIMAL(p,s)` |
| Boolean | `BOOLEAN` | `TINYINT(1)` | `INTEGER` 0/1 | `BIT` |
| UUID | `UUID` | `BINARY(16)` or `CHAR(36)` | `TEXT`/`BLOB` | `UNIQUEIDENTIFIER` |
| Array | `TYPE[]` | none (JSON) | none (JSON) | none (JSON) |
| JSON | `JSONB` | `JSON` | JSON functions over `TEXT` | `NVARCHAR` + JSON functions |
| Enum | native `ENUM` type | inline `ENUM(...)` | none | none |
| Unsigned integers | none | yes | none | none |
| IP / network | `INET`, `CIDR` | none | none | none |
SQLite's type affinity means declared types are advisory: an `INTEGER` column accepts `'abc'` unless the table is declared `STRICT` (>=3.37). Any migration out of SQLite must profile actual values, not declared types.
MySQL cannot index a `TEXT` column without a prefix length (`INDEX (col(191))`), and the 191 convention comes from the old 767-byte index limit under `utf8mb4` — modern InnoDB with `DYNAMIC` row format allows 3072 bytes, so the limit is often no longer needed.
## Auto-Increment Behavior
| Engine | Syntax | Gaps | Reset after load |
|---|---|---|---|
| PostgreSQL | `GENERATED ALWAYS AS IDENTITY` (prefer over `SERIAL`) | Sequences are non-transactional: rollbacks consume values | `SELECT setval('t_id_seq', (SELECT MAX(id) FROM t))` |
| MySQL | `AUTO_INCREMENT` | Gaps on rollback; InnoDB may reset the counter on restart in older versions | `ALTER TABLE t AUTO_INCREMENT = n` |
| SQLite | `INTEGER PRIMARY KEY` (rowid alias) | Reuses deleted maximum values unless `AUTOINCREMENT` is declared | Managed via `sqlite_sequence` |
| SQL Server | `IDENTITY(1,1)` | Large jumps possible after restart (identity cache) | `DBCC CHECKIDENT` |
Gaps in generated ids are normal in every engine. Treat them as meaningless: any business logic that counts on contiguous ids (invoice numbering, "records processed") is already broken and needs its own sequence table.
## Upsert
```sql
-- PostgreSQL / SQLite
INSERT INTO t (k, v) VALUES (?, ?)
ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v;
-- MySQL
INSERT INTO t (k, v) VALUES (?, ?)
ON DUPLICATE KEY UPDATE v = VALUES(v); -- MySQL >=8.0.20: v = new.v
-- SQL Server (and PostgreSQL >=15)
MERGE INTO t AS tgt USING (VALUES (?, ?)) AS src(k, v) ON tgt.k = src.k
WHEN MATCHED THEN UPDATE SET v = src.v
WHEN NOT MATCHED THEN INSERT (k, v) VALUES (src.k, src.v);
```
`ON DUPLICATE KEY` fires on **any** unique constraint, not just the one you had in mind — a row can update through a different key than expected. `ON CONFLICT (k)` names the constraint explicitly, which is why it is the safer construct.
## Limit, Offset, Top
```sql
SELECT ... ORDER BY id LIMIT 20 OFFSET 40; -- PostgreSQL, MySQL, SQLite
SELECT TOP 20 ... ORDER BY id; -- SQL Server, no offset
SELECT ... ORDER BY id OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY; -- SQL Server 2012+, standard
```
SQL Server requires `ORDER BY` for `OFFSET ... FETCH`. Every engine returns undefined order without `ORDER BY`, and a unique tiebreaker is required for stable pagination.
## Returning Affected Rows
- PostgreSQL and SQLite (>=3.35): `INSERT/UPDATE/DELETE ... RETURNING *`.
- SQL Server: `OUTPUT INSERTED.*` / `OUTPUT DELETED.*`, which can also write into a table.
- MySQL: none. Use `LAST_INSERT_ID()` for a single insert, or re-select. MariaDB has `RETURNING`.
Multi-row inserts on MySQL give you the first generated id from `LAST_INSERT_ID()`; the rest are consecutive only when `innodb_autoinc_lock_mode` guarantees it. Batch inserts needing ids back should insert client-generated keys instead.
## GROUP BY Strictness
- Standard, PostgreSQL, and SQL Server: every selected non-aggregated column must appear in `GROUP BY` (PostgreSQL relaxes this when you group by the primary key, since everything else is functionally dependent).
- MySQL historically allowed selecting arbitrary ungrouped columns, returning an unspecified row's value. `ONLY_FULL_GROUP_BY` is in the default `sql_mode` from 5.7 — legacy queries fail on upgrade, and the fix is usually `ANY_VALUE()` or a proper aggregate.
- SQLite permits bare columns and documents that with `MIN`/`MAX` the bare columns come from the matching row — convenient, non-portable.
## Window Functions and CTEs
| Feature | Floor |
|---|---|
| Window functions | PostgreSQL 8.4, MySQL 8.0, MariaDB 10.2, SQLite 3.25, SQL Server 2005 |
| Recursive CTE | PostgreSQL 8.4, MySQL 8.0, SQLite 3.8.3, SQL Server 2005 |
| `FILTER (WHERE ...)` on aggregates | PostgreSQL, SQLite 3.30; MySQL and SQL Server need `CASE` |
| `GROUPING SETS`/`CUBE`/`ROLLUP` | PostgreSQL 9.5, SQL Server, MySQL (`WITH ROLLUP` only), SQLite none |
| Generated columns | PostgreSQL 12 (stored only), MySQL 5.7, SQLite 3.31, SQL Server (computed) |
| Partitioned tables | PostgreSQL 10 declarative, MySQL native, SQL Server, SQLite none |
| Full-text | PostgreSQL tsvector, MySQL InnoDB FTS, SQLite FTS5, SQL Server FTS |
CTE materialization also differs: PostgreSQL inlined CTEs from version 12 (before that every CTE was an optimization fence), MySQL 8 treats them like derived tables, SQL Server always inlines. A query that relied on the fence for performance changes behavior on upgrade.
## DDL Transactionality
PostgreSQL, SQLite, and SQL Server run DDL inside transactions — a failed migration rolls back cleanly. MySQL and MariaDB commit implicitly before and after each DDL statement, so a multi-statement migration can end half-applied with no rollback available.
Consequences for MySQL specifically: one DDL statement per migration file, a verified backup before schema changes, and online schema-change tooling (`gh-ost`, `pt-online-schema-change`) for large tables.
## Booleans, Concatenation, Math
- Booleans: PostgreSQL has a real type; MySQL's `TRUE`/`FALSE` are literals for 1/0 in a `TINYINT`; SQL Server has `BIT` and no boolean expression type, so `SELECT (a > b)` is invalid there and needs `CASE`.
- Concatenation: `||` in PostgreSQL and SQLite; `CONCAT()` in MySQL (where `||` means logical OR unless `PIPES_AS_CONCAT` is set); `+` or `CONCAT()` in SQL Server.
- Integer division truncates in PostgreSQL, MySQL, and SQL Server; SQLite also truncates for integer operands. Cast one operand to get a real division.
- Modulo of negatives, rounding mode (half-up vs half-even), and `AVG` of integers all vary. Cast to a decimal type before any arithmetic whose result is reported to a user.
## Error Behavior on Bad Data
- Inserting a too-long string: PostgreSQL and SQL Server error; MySQL errors in strict mode (default since 5.7) and truncated without warning before that.
- Invalid dates: MySQL historically accepted `'0000-00-00'`; strict mode rejects it, but old data survives upgrades and breaks every migration out.
- Division by zero: PostgreSQL, SQL Server, and SQLite raise an error; MySQL returns NULL unless `ERROR_FOR_DIVISION_BY_ZERO` is in `sql_mode`.
- Out-of-range numerics: error in PostgreSQL, clamped or rejected in MySQL depending on mode.
When auditing a MySQL database, read `SELECT @@sql_mode` first — it determines which of these are errors and which are corruption with no error.
## Portability Strategy
- **One target engine.** The portable subset is small, and code written for "any database" is usually written well for none. Support a second engine only when a paying requirement says so.
- If you must be portable: no vendor types, no engine-specific functions in shared code, ANSI `CASE` instead of `IF`/`IIF`, explicit `CAST`, no `RETURNING`, no arrays, no upsert syntax — push the differences into a thin per-engine layer.
- Test against the real target engine, not SQLite-in-memory standing in for PostgreSQL: SQLite accepts statements the target rejects, and enforces less.
- Keep a written list of the version floors you rely on (`engine_version` in Configuration) so an upgrade is a checklist, not an archaeology project.