rustango 0.60.7

Batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
//! The `Dialect` trait, with one implementation per backend. Every
//! SQL writer dispatches through it.
//!
//! Its methods fall into three groups:
//!
//! * **Compilation** — `compile_select` and friends lower the
//!   dialect-neutral query IR to a [`CompiledStatement`]. Always
//!   overridden.
//! * **DDL primitives** — `quote_ident`, `placeholder`,
//!   `serial_type`, `bool_literal` and the rest, used by
//!   `migrate::ddl`. Most have an ANSI-shaped default; a dialect
//!   overrides only what differs.
//! * **Identity** — `name()`, for error messages and logs.

use crate::core::{
    AggregateQuery, BulkInsertQuery, BulkUpdateQuery, ConflictClause, CountQuery, DeleteQuery,
    FieldType, InsertQuery, ModelSchema, Op, SelectQuery, UpdateQuery,
};

use super::{CompiledStatement, SqlError};

/// Write `<col> ?| ARRAY[$1, $2, …]`, shared by both default
/// JSON-key methods.
fn write_pg_array_keys(
    sql: &mut String,
    qualified_col: &str,
    placeholders: &[String],
    keyword: &str,
) {
    sql.push_str(qualified_col);
    sql.push_str(keyword);
    let mut first = true;
    for p in placeholders {
        if !first {
            sql.push_str(", ");
        }
        first = false;
        sql.push_str(p);
    }
    sql.push(']');
}

/// `true` for the canonical random-UUID DEFAULT (`auto_uuid`), which only
/// Postgres has as a function (#1987).
pub(crate) fn is_uuid_expr(expr: &str) -> bool {
    expr.trim().eq_ignore_ascii_case("gen_random_uuid()")
}

/// `name` in `q` quotes with any `q` inside doubled, in one allocation.
/// Every compiled query quotes each identifier, so this is hot (#2662).
pub(crate) fn quote_with(name: &str, q: char) -> String {
    let mut out = String::with_capacity(name.len() + 2);
    out.push(q);
    for part in name.split_inclusive(q) {
        out.push_str(part);
        if part.ends_with(q) {
            out.push(q);
        }
    }
    out.push(q);
    out
}

/// Turns the dialect-neutral query IR into a parameterized statement,
/// and supplies the DDL primitives the migration runner needs.
///
/// It is `Send + Sync` because the migration runner holds a
/// `&'static dyn Dialect` across `await` points. Every implementor is
/// a unit struct, so this costs nothing.
pub trait Dialect: Send + Sync {
    /// This dialect's short name: `"postgres"`, `"sqlite"` or
    /// `"mysql"`. For error messages and logs.
    fn name(&self) -> &'static str;

    // ---- DDL primitives, defaulting to the ANSI shape ----

    /// Quote a table or column name. The default is ANSI
    /// double-quotes, which suit PG and SQLite; MySQL uses backticks.
    /// A quote inside the name is doubled, so the result is safe.
    fn quote_ident(&self, name: &str) -> String {
        quote_with(name, '"')
    }

    /// Quote text as an inline string literal, for the few spots
    /// that take no bind. The default doubles `'`; MySQL also escapes `\`.
    fn quote_literal(&self, text: &str) -> String {
        format!("'{}'", text.replace('\'', "''"))
    }

    /// Render the placeholder for the `n`-th bind, counting from 1.
    ///
    /// **`n` is advisory.** Only PostgreSQL uses it, as `$n`. SQLite
    /// and MySQL ignore it and emit `?`, so on those backends the
    /// binds must be pushed in the order their placeholders appear
    /// **in the SQL text**. Take `n` from the bind vector's length as
    /// you push, never from a separate counter.
    ///
    /// Two mistakes follow from ignoring that, and both are silent on
    /// PostgreSQL:
    ///
    /// - **Binds out of order.** In `SET ts = {p1} WHERE id IN ({p2})`,
    ///   pushing the timestamp last binds an id into `ts`. The count
    ///   matches, so there is no error, just wrong rows.
    /// - **Reusing a number.** `$1` twice is one bind on PostgreSQL,
    ///   but two `?` needing two binds elsewhere.
    ///
    /// `sql::writers::Sql::push_param` cannot get this wrong, because
    /// it pushes the value and then reads `params.len()`. It is
    /// private to `sql`, so code outside writes the pattern by hand.
    fn placeholder(&self, n: usize) -> String {
        let _ = n;
        "?".to_owned()
    }

    /// The column type for an `Auto<T>` primary key. `field_type` is
    /// `I32` or `I64`; the derive already rejects `Auto<T>` on other
    /// types.
    ///
    /// The default is plain `INTEGER` / `BIGINT`. PG uses `SERIAL`
    /// and `BIGSERIAL`, SQLite `INTEGER PRIMARY KEY AUTOINCREMENT`,
    /// MySQL `BIGINT AUTO_INCREMENT`.
    fn serial_type(&self, field_type: FieldType) -> &'static str {
        match field_type {
            FieldType::I32 => "INTEGER",
            _ => "BIGINT",
        }
    }

    /// `true` when [`Self::serial_type`] already includes
    /// `PRIMARY KEY`, as SQLite does: it needs
    /// `INTEGER PRIMARY KEY AUTOINCREMENT` as one phrase. Otherwise
    /// the DDL writer appends `PRIMARY KEY` itself.
    fn serial_type_includes_primary_key(&self) -> bool {
        false
    }

    /// The column type for a non-`Auto` integer primary key that has a
    /// DB default, when [`Self::column_type`] would make the DB ignore
    /// that default. `None` keeps [`Self::column_type`].
    fn defaulted_integer_pk_type(&self) -> Option<&'static str> {
        None
    }

    /// `true` when foreign keys must go inside `CREATE TABLE` instead
    /// of a later `ALTER TABLE … ADD CONSTRAINT`.
    ///
    /// PG and MySQL add them afterwards, so tables that reference
    /// each other resolve across the batch. SQLite has no
    /// `ADD CONSTRAINT`, so its FKs must be in the CREATE.
    fn inline_fks_in_create_table(&self) -> bool {
        false
    }

    /// `true` when changing a table's constraints means rebuilding the
    /// table (SQLite), not an `ALTER` (#1557, #1982).
    fn alters_by_rebuild(&self) -> bool {
        false
    }

    /// `true` if the server enforces `ON DELETE SET DEFAULT`. InnoDB
    /// accepts the clause and then refuses the parent delete (#1573).
    fn supports_on_delete_set_default(&self) -> bool {
        true
    }

    /// `true` when changing a column restates its whole definition
    /// (MySQL's `MODIFY COLUMN`), not one property at a time (#1676).
    fn modifies_whole_column(&self) -> bool {
        false
    }

    /// How many binds the backend takes in one statement. A multi-row
    /// `INSERT` reaches this at `rows × columns`.
    ///
    /// * **Postgres** — 65535, a hard protocol limit.
    /// * **SQLite** — 32766 since 3.32.
    /// * **MySQL** — no fixed cap, only `max_allowed_packet`; 65535
    ///   sits well inside the default.
    fn max_bind_params(&self) -> usize {
        65535
    }

    /// Translate a `DEFAULT` expression from the canonical Postgres
    /// form into this backend's spelling. `ty` is the field type
    /// token, since the right syntax can depend on the column type:
    /// MySQL, for instance, needs `DEFAULT ('{}')` on a JSON column.
    ///
    /// The default passes the expression through. SQLite turns
    /// `now()` into a `strftime` call and drops `::type` casts.
    ///
    /// `max_length` matters only to MySQL, which needs it to tell an
    /// unbounded `String` (a `LONGTEXT` column, which allows no literal
    /// default) from a bounded one (a `VARCHAR(n)`, which does).
    fn translate_default_expr(&self, expr: &str, _ty: &str, _max_length: Option<u32>) -> String {
        expr.to_owned()
    }

    /// The `DEFAULT` expression meaning "the time of the write".
    ///
    /// Hand-written DDL should call this rather than spell it out: on
    /// SQLite it is not a keyword but a `strftime` call in the
    /// canonical format.
    ///
    /// It is only a backstop. Every writer binds its own timestamp.
    fn current_timestamp_default(&self) -> String {
        self.translate_default_expr("now()", "datetime", None)
    }

    /// The full column clause for such a timestamp:
    /// `<type> NOT NULL DEFAULT <now>`.
    fn timestamp_now_column(&self) -> String {
        format!(
            "{} NOT NULL DEFAULT {}",
            self.column_type(FieldType::DateTime, None),
            self.current_timestamp_default()
        )
    }

    /// A boolean literal for `DEFAULT` clauses and inline
    /// comparisons. `TRUE` / `FALSE` by default; SQLite and MySQL use
    /// `1` / `0`, having no boolean type.
    fn bool_literal(&self, b: bool) -> &'static str {
        if b {
            "TRUE"
        } else {
            "FALSE"
        }
    }

    /// `true` if `CREATE INDEX CONCURRENTLY` works. Only Postgres.
    /// Elsewhere a non-atomic migration falls back to a plain
    /// `CREATE INDEX` with a warning.
    fn supports_concurrent_index(&self) -> bool {
        false
    }

    /// `true` if `CREATE INDEX IF NOT EXISTS` is accepted. MySQL has
    /// no such form, so the renderer drops the guard there. The
    /// migration ledger already prevents re-runs.
    fn supports_create_index_if_not_exists(&self) -> bool {
        true
    }

    /// The `USING <method>` clause in
    /// `CREATE INDEX … ON tbl <USING …> (cols)`. Empty for the
    /// default btree, and for SQLite, which has no `USING`.
    ///
    /// An implementation must check the method token and return `""`
    /// for one the backend does not know, so the index is still
    /// created as a btree.
    fn index_method_clause(&self, method: &str) -> String {
        match method {
            "" | "btree" => String::new(),
            other => format!(" USING {other}"),
        }
    }

    /// The "insert or skip" tail for an `INSERT`, so the row is
    /// written the first time and skipped on a re-run.
    ///
    /// `conflict_cols` are **already-quoted** identifiers naming the
    /// unique constraint to match on.
    ///
    /// The default is `ON CONFLICT (…) DO NOTHING`. MySQL uses
    /// `ON DUPLICATE KEY UPDATE <col> = <col>`, a no-op write that
    /// satisfies its requirement for at least one SET expression.
    fn insert_on_conflict_skip(&self, conflict_cols: &[&str]) -> String {
        if conflict_cols.is_empty() {
            return String::new();
        }
        format!("ON CONFLICT ({}) DO NOTHING", conflict_cols.join(", "))
    }

    /// The `INSERT` tail when no column is named (#2416).
    fn default_values_clause(&self) -> &'static str {
        " DEFAULT VALUES"
    }

    /// The VALUES cell asking for `pk`'s default in a multi-row insert (#2416).
    fn default_pk_cell(&self, _pk: &crate::core::FieldSchema) -> &'static str {
        "DEFAULT"
    }

    /// `true` if `INSERT … RETURNING` works: always on Postgres,
    /// from 3.35 on SQLite, never on MySQL. When it is `false`, an
    /// `Auto<T>` insert has to read the id back with a second query.
    fn supports_returning(&self) -> bool {
        false
    }

    /// `true` if `ORDER BY … NULLS FIRST|LAST` is understood. MySQL
    /// is the exception, so the writer sorts on `<col> IS NULL`
    /// first: `DESC` on that term for NULLS FIRST, `ASC` for
    /// NULLS LAST.
    fn supports_nulls_order(&self) -> bool {
        true
    }

    /// The name of the random-number function. `RANDOM` on PG and
    /// SQLite, `RAND` on MySQL.
    fn random_fn(&self) -> &'static str {
        "RANDOM"
    }

    /// The `LIMIT` clause to add when a query has an `OFFSET` but no
    /// `LIMIT`. The writer puts it before the `OFFSET`.
    ///
    /// MySQL and SQLite demand a `LIMIT` alongside any `OFFSET`; PG
    /// accepts a bare `OFFSET` and returns `None`.
    fn offset_without_limit_clause(&self) -> Option<&'static str> {
        None
    }

    /// `true` if `IN (<subquery>)` rejects a `LIMIT`/`OFFSET` inside
    /// (MySQL 1235), so the writer wraps it in a derived table.
    fn in_subquery_limit_needs_derived_table(&self) -> bool {
        false
    }

    /// Cast a SUM back to BIGINT. PG returns NUMERIC and MySQL
    /// DECIMAL, neither of which the row decoder reads.
    fn cast_aggregate_to_int(&self, expr: &str) -> String {
        format!("CAST({expr} AS BIGINT)")
    }

    /// Wrap an AVG expression in a cast to a floating-point type.
    /// PostgreSQL AVG of any int returns NUMERIC; MySQL AVG returns
    /// DOUBLE for ints, DECIMAL for decimals. Default: ANSI
    /// `CAST(<expr> AS DOUBLE PRECISION)`.
    fn cast_aggregate_to_float(&self, expr: &str) -> String {
        format!("CAST({expr} AS DOUBLE PRECISION)")
    }

    /// The type to cast a `NULL` parameter to when the column type is
    /// known. Postgres needs it: an untyped `NULL` bound against an
    /// integer column is rejected, and `$1::INTEGER` says which NULL
    /// is meant. Other dialects return `None` and the writer skips
    /// the cast.
    fn null_cast(&self, ty: FieldType) -> Option<&'static str> {
        let _ = ty;
        None
    }

    /// `ALTER TABLE <table> DROP …` for a named CHECK constraint, or
    /// `None` on a dialect with no such statement, as on SQLite.
    ///
    /// Every dialect must answer for itself. Postgres spells it
    /// `DROP CONSTRAINT IF EXISTS`, which is idempotent. MySQL spells
    /// it `DROP CHECK` and takes no `IF EXISTS`, so it errors when
    /// the constraint is already gone.
    ///
    /// # Panics
    /// The default body panics on purpose, naming the dialect. There
    /// is no safe fallback: handing a new backend the PostgreSQL form
    /// once shipped SQL that MySQL could not parse, and a panic on
    /// the first migration is easier to find than a syntax error from
    /// a live server.
    fn drop_check_constraint_sql(&self, table: &str, name: &str) -> Option<String> {
        let _ = (table, name);
        unimplemented!(
            "Dialect::drop_check_constraint_sql is not implemented for `{}`. \
             Implement it: return the dialect's own `ALTER TABLE … DROP …` \
             spelling, or `None` if it has none. There is deliberately no \
             fallback — inheriting PostgreSQL's form is what shipped invalid \
             SQL to MySQL in #559.",
            self.name()
        )
    }

    /// `ALTER TABLE <table> DROP …` for a named foreign key, or
    /// `None` where there is no such statement.
    ///
    /// Postgres uses `DROP CONSTRAINT IF EXISTS`, MySQL the
    /// non-idempotent `DROP FOREIGN KEY`.
    ///
    /// # Panics
    /// As [`Dialect::drop_check_constraint_sql`].
    fn drop_foreign_key_sql(&self, table: &str, name: &str) -> Option<String> {
        let _ = (table, name);
        unimplemented!(
            "Dialect::drop_foreign_key_sql is not implemented for `{}`. \
             Implement it: return the dialect's own `ALTER TABLE … DROP …` \
             spelling, or `None` if it has none.",
            self.name()
        )
    }

    /// Query for the names of the FKs on one column, binding `(table,
    /// column)`. Single-column FKs only: a composite one stays (#1557).
    /// `None` where FKs cannot be dropped by name (SQLite).
    fn foreign_key_names_sql(&self) -> Option<&'static str> {
        None
    }

    /// Query for an index's first column when no other index starts with it,
    /// binding `(table, index)`, where an FK refuses to lose the only index
    /// it can use (MySQL, 1553; #2244).
    fn sole_leading_column_sql(&self) -> Option<&'static str> {
        None
    }

    /// Query for the multi-column FKs on `table` that only `index` can serve,
    /// binding `(table, index, index)` (MySQL, 1553; #2326).
    fn composite_fks_needing_index_sql(&self) -> Option<&'static str> {
        None
    }

    /// Query for the names of the single-column UNIQUE indexes on one
    /// column, binding `(table, column)`. `Some` where an `AlterColumnUnique`
    /// drops the index by that name (MySQL) (#1676).
    fn unique_index_names_sql(&self) -> Option<&'static str> {
        None
    }

    /// Drop the UNIQUE index `name` from `table`, for
    /// [`Self::unique_index_names_sql`]'s names.
    fn drop_unique_index_sql(&self, table: &str, name: &str) -> Option<String> {
        let _ = (table, name);
        None
    }

    /// Statement that moves `table.column`'s serial counter past the rows
    /// already there, binding `(quoted table, column)` in that order.
    /// `None` where the counter follows explicit ids by itself (MySQL,
    /// SQLite); Postgres' sequence does not, so `loaddata` needs it (#1911).
    fn reset_sequence_sql(&self, table: &str, column: &str) -> Option<String> {
        let _ = (table, column);
        None
    }

    /// Statement that defers FK checks to `COMMIT` for the current
    /// transaction only. Default (SQLite): the pragma; `None` where no
    /// transaction-scoped form exists.
    fn defer_foreign_keys_sql(&self) -> Option<&'static str> {
        Some("PRAGMA defer_foreign_keys = ON")
    }

    /// Statements that empty `tables` in any order, run in one
    /// transaction. Default (SQLite): FK checks deferred to `COMMIT`.
    fn clear_tables_sql(&self, tables: &[&str]) -> Vec<String> {
        self.defer_foreign_keys_sql()
            .map(str::to_owned)
            .into_iter()
            .chain(
                tables
                    .iter()
                    .map(|t| format!("DELETE FROM {}", self.quote_ident(t))),
            )
            .collect()
    }

    /// Add a named UNIQUE on one column of an existing table. PG and MySQL
    /// share this form; SQLite has no `ADD CONSTRAINT` and overrides it.
    fn add_unique_constraint_sql(&self, table: &str, name: &str, column: &str) -> String {
        format!(
            "ALTER TABLE {} ADD CONSTRAINT {} UNIQUE ({})",
            self.quote_ident(table),
            self.quote_ident(name),
            self.quote_ident(column)
        )
    }

    /// `true` if partial indexes, `CREATE INDEX … WHERE <expr>`, are
    /// supported. MySQL has no equivalent, so the migration writer
    /// drops the WHERE clause there and warns.
    fn supports_partial_index(&self) -> bool {
        true
    }

    /// Does this dialect advertise the given feature token?
    ///
    /// `manage check --deploy` reads every model's
    /// `required_db_features` and warns where this says `false`, so a
    /// project can declare a need like `listen_notify` and hear about
    /// it before deploying rather than at runtime.
    ///
    /// The default covers what all backends share; each dialect adds
    /// its own on top. An unknown token is `false`, so the warning
    /// fires.
    #[must_use]
    fn supports(&self, token: &str) -> bool {
        self.default_supports(token)
    }

    /// The tokens every backend supports. An override of
    /// [`Self::supports`] calls this and adds its own.
    #[must_use]
    fn default_supports(&self, token: &str) -> bool {
        matches!(
            token,
            "window_functions" | "recursive_cte" | "cte" | "json_extract" | "expression_index"
        ) || (token == "partial_index" && self.supports_partial_index())
            || (token == "returning" && self.supports_returning())
    }

    /// The type token for `CAST(<expr> AS <ty>)`. `None` when the
    /// dialect cannot cast to that type at all, as SQLite cannot to
    /// UUID or JSONB; the caller then errors.
    ///
    /// This is neither [`Self::null_cast`], which is PG-only, nor
    /// [`Self::column_type`], which carries lengths for DDL.
    fn cast_type(&self, ty: FieldType) -> Option<&'static str> {
        Some(match ty {
            FieldType::I16 => "SMALLINT",
            FieldType::I32 => "INTEGER",
            FieldType::I64 => "BIGINT",
            FieldType::F32 => "REAL",
            FieldType::F64 => "DOUBLE PRECISION",
            FieldType::Bool => "BOOLEAN",
            FieldType::String => "TEXT",
            FieldType::DateTime => "TIMESTAMPTZ",
            FieldType::Date => "DATE",
            FieldType::Uuid => "UUID",
            FieldType::Json => "JSONB",
            FieldType::Decimal => "NUMERIC",
            FieldType::Binary => "BYTEA",
            FieldType::Time => "TIME",
            FieldType::Array(crate::core::ArrayElem::Text) => "text[]",
            FieldType::Array(crate::core::ArrayElem::Int) => "integer[]",
            FieldType::Array(crate::core::ArrayElem::BigInt) => "bigint[]",
            FieldType::Range(crate::core::RangeElem::Int) => "int4range",
            FieldType::Range(crate::core::RangeElem::BigInt) => "int8range",
            FieldType::Range(crate::core::RangeElem::Numeric) => "numrange",
            FieldType::Range(crate::core::RangeElem::Date) => "daterange",
            FieldType::Range(crate::core::RangeElem::DateTime) => "tstzrange",
            FieldType::HStore => "hstore",
            // `vector(N)` carries a runtime value in the type, so it has
            // no fixed CAST spelling; bulk_update casts its NULLs itself.
            FieldType::Vector(_) => return None,
            // PostGIS casts text to geometry implicitly, so a NULL needs no cast.
            FieldType::Geometry(_) => return None,
        })
    }

    /// The `CREATE TABLE` column type for a field that is not an
    /// `Auto<T>` PK. `max_length` turns a [`FieldType::String`] into
    /// `VARCHAR(N)` instead of unbounded text.
    ///
    /// The default uses Postgres names. MySQL overrides most of them:
    /// it has no `TIMESTAMPTZ`, `JSONB` or `UUID`, and spells
    /// `BOOLEAN` as `TINYINT(1)`.
    fn column_type(&self, ty: FieldType, max_length: Option<u32>) -> String {
        match ty {
            FieldType::I16 => "SMALLINT".into(),
            FieldType::I32 => "INTEGER".into(),
            FieldType::I64 => "BIGINT".into(),
            FieldType::F32 => "REAL".into(),
            FieldType::F64 => "DOUBLE PRECISION".into(),
            FieldType::Bool => "BOOLEAN".into(),
            FieldType::String => match max_length {
                Some(n) => format!("VARCHAR({n})"),
                None => "TEXT".into(),
            },
            FieldType::DateTime => "TIMESTAMPTZ".into(),
            FieldType::Date => "DATE".into(),
            FieldType::Time => "TIME".into(),
            FieldType::Uuid => "UUID".into(),
            FieldType::Json => "JSONB".into(),
            FieldType::Decimal => "NUMERIC".into(),
            FieldType::Binary => "BYTEA".into(),
            // An array type has no length cap, so `max_length` is
            // ignored here.
            FieldType::Array(elem) => format!("{}[]", elem.pg_element_type()),
            FieldType::Range(elem) => elem.pg_range_type().to_owned(),
            // Needs the `hstore` extension.
            FieldType::HStore => "hstore".into(),
            // Needs the `vector` extension. 0 dimensions means the
            // column takes any size.
            FieldType::Vector(0) => "vector".into(),
            FieldType::Vector(dims) => format!("vector({dims})"),
            // Needs PostGIS. SRID 0 means no SRID constraint.
            FieldType::Geometry(0) => "geometry(Point)".into(),
            FieldType::Geometry(srid) => format!("geometry(Point,{srid})"),
        }
    }

    /// The column type for a case-insensitive text field: `CITEXT` on
    /// Postgres, `TEXT COLLATE NOCASE` on SQLite, a
    /// `utf8mb4_general_ci` collation on MySQL.
    ///
    /// The default is plain `TEXT`, which leaves case handling to the
    /// query. Use `LOWER(…)` there on a dialect that does not
    /// override this.
    fn ci_text_type(&self, max_length: Option<u32>) -> String {
        let _ = max_length;
        "TEXT".to_owned()
    }

    /// DDL a migration runs before it writes a case-insensitive column,
    /// such as `CREATE EXTENSION IF NOT EXISTS citext SCHEMA public` on
    /// Postgres. `None` when nothing is needed.
    fn ci_text_extension_sql(&self) -> Option<&'static str> {
        None
    }

    /// A one-row, one-column query for the database's default collation,
    /// which `check --deploy` reads. `None` where text compares byte-wise.
    fn default_collation_sql(&self) -> Option<&'static str> {
        None
    }

    /// `true` if this dialect can write `op` as SQL. Return `false`
    /// for an operator with no equivalent; the writer then reports
    /// [`SqlError::OperatorNotSupportedInDialect`].
    fn supports_op(&self, op: Op) -> bool {
        let _ = op;
        true
    }

    // ---- per-operator predicate writers ----
    //
    // Each of these gets the column already rendered and quoted, plus
    // a placeholder string, and composes the fragment its parser
    // wants. The defaults are the Postgres shape.

    /// Case-insensitive LIKE: `<col> ILIKE <p>` on Postgres,
    /// `LOWER(<col>) LIKE LOWER(<p>)` elsewhere.
    fn write_ilike(&self, sql: &mut String, qualified_col: &str, placeholder: &str, negated: bool) {
        sql.push_str(qualified_col);
        sql.push_str(if negated { " NOT ILIKE " } else { " ILIKE " });
        sql.push_str(placeholder);
    }

    /// `qualified_col` as text a `LIKE` can match, given its field type
    /// (`None` when unknown). Postgres casts a non-text column (#2229).
    fn like_operand(&self, qualified_col: &str, ty: Option<FieldType>) -> String {
        let _ = ty;
        qualified_col.to_owned()
    }

    /// [`Self::write_ilike`] on [`Self::like_operand`].
    fn write_ilike_typed(
        &self,
        sql: &mut String,
        qualified_col: &str,
        ty: Option<FieldType>,
        placeholder: &str,
        negated: bool,
    ) {
        let col = self.like_operand(qualified_col, ty);
        self.write_ilike(sql, &col, placeholder, negated);
    }

    /// Case-sensitive `<col> [NOT] LIKE <p>` on [`Self::like_operand`] (#2263).
    fn write_like_typed(
        &self,
        sql: &mut String,
        qualified_col: &str,
        ty: Option<FieldType>,
        placeholder: &str,
        negated: bool,
    ) {
        sql.push_str(&self.like_operand(qualified_col, ty));
        sql.push_str(if negated { " NOT LIKE " } else { " LIKE " });
        sql.push_str(placeholder);
    }

    /// POSIX regex match, for the `__regex` and `__iregex` lookups.
    ///
    /// The default is Postgres' `~`, `!~`, `~*` and `!~*`. MySQL and
    /// SQLite use `REGEXP` and `NOT REGEXP`; neither has a
    /// case-insensitive form, so both wrap the column and the
    /// placeholder in `LOWER(…)` for those variants.
    fn write_regex(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholder: &str,
        case_sensitive: bool,
        negated: bool,
    ) {
        sql.push_str(qualified_col);
        let op = match (case_sensitive, negated) {
            (true, false) => " ~ ",
            (true, true) => " !~ ",
            (false, false) => " ~* ",
            (false, true) => " !~* ",
        };
        sql.push_str(op);
        sql.push_str(placeholder);
    }

    /// Trigram similarity, for the `__trigram_similar` and
    /// `__trigram_word_similar` lookups. Writes `<col> % <p>`, or
    /// `<col> %> <p>` when `word`. Needs the `pg_trgm` extension.
    ///
    /// # Errors
    /// [`SqlError::OpNotSupportedInDialect`] on MySQL and SQLite,
    /// which have nothing like it.
    fn write_trigram_similar(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholder: &str,
        word: bool,
    ) -> Result<(), super::SqlError> {
        sql.push_str(qualified_col);
        sql.push_str(if word { " %> " } else { " % " });
        sql.push_str(placeholder);
        Ok(())
    }

    /// Full-text search, for the `__search` lookup. Writes
    /// `to_tsvector(<col>) @@ plainto_tsquery(<p>)`, so the database
    /// picks the config from `default_text_search_config`.
    ///
    /// # Errors
    /// [`SqlError::OpNotSupportedInDialect`] on MySQL and SQLite.
    /// Their full-text search works differently enough that this
    /// shape has no meaning there.
    fn write_search(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholder: &str,
    ) -> Result<(), super::SqlError> {
        sql.push_str("to_tsvector(");
        sql.push_str(qualified_col);
        sql.push_str(") @@ plainto_tsquery(");
        sql.push_str(placeholder);
        sql.push(')');
        Ok(())
    }

    /// Array containment and overlap: `@>`, `<@` and `&&`. `op` is
    /// the operator itself, and the result is
    /// `<col> <op> <placeholder>`.
    ///
    /// # Errors
    /// `OpNotSupportedInDialect` when the dialect doesn't support
    /// PG array operators.
    fn write_array_op(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholder: &str,
        op: &'static str,
    ) -> Result<(), super::SqlError> {
        sql.push_str(qualified_col);
        sql.push(' ');
        sql.push_str(op);
        sql.push(' ');
        sql.push_str(placeholder);
        Ok(())
    }

    /// Range operators: `@>`, `<@`, `&&`, `<<`, `>>` and `-|-`. `op`
    /// is the operator itself, and the result is
    /// `<col> <op> <placeholder>`.
    ///
    /// # Errors
    /// [`SqlError::OpNotSupportedInDialect`] on MySQL and SQLite,
    /// which have no range type.
    fn write_range_op(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholder: &str,
        op: &'static str,
    ) -> Result<(), super::SqlError> {
        sql.push_str(qualified_col);
        sql.push(' ');
        sql.push_str(op);
        sql.push(' ');
        sql.push_str(placeholder);
        Ok(())
    }

    /// Null-safe equality. Postgres: `<col> IS [NOT] DISTINCT FROM <p>`.
    /// `distinct = true` means "not equal under null-safe semantics".
    fn write_null_safe_eq(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholder: &str,
        distinct: bool,
    ) {
        sql.push_str(qualified_col);
        sql.push_str(if distinct {
            " IS DISTINCT FROM "
        } else {
            " IS NOT DISTINCT FROM "
        });
        sql.push_str(placeholder);
    }

    /// Write the placeholder of a bound JSON value. PG and SQLite take
    /// it as is; MySQL wraps it, since sqlx binds JSON as text.
    fn write_json_param(&self, sql: &mut String, placeholder: &str) {
        sql.push_str(placeholder);
    }

    /// JSON containment: `<col> @> <p>::jsonb` (Postgres) /
    /// `JSON_CONTAINS(<col>, <p>)` (MySQL).
    fn write_json_contains(&self, sql: &mut String, qualified_col: &str, placeholder: &str) {
        sql.push_str(qualified_col);
        sql.push_str(" @> ");
        sql.push_str(placeholder);
        sql.push_str("::jsonb");
    }

    /// The inverse of [`Self::write_json_contains`]:
    /// `<col> <@ <p>::jsonb` on Postgres, `JSON_CONTAINS(<p>, <col>)`
    /// on MySQL, with the arguments the other way round.
    fn write_json_contained_by(&self, sql: &mut String, qualified_col: &str, placeholder: &str) {
        sql.push_str(qualified_col);
        sql.push_str(" <@ ");
        sql.push_str(placeholder);
        sql.push_str("::jsonb");
    }

    /// Top-level JSON key existence: `<col> ? <p>` (Postgres) /
    /// `JSON_CONTAINS_PATH(<col>, 'one', CONCAT('$.', <p>))` (MySQL).
    fn write_json_has_key(&self, sql: &mut String, qualified_col: &str, placeholder: &str) {
        sql.push_str(qualified_col);
        sql.push_str(" ? ");
        sql.push_str(placeholder);
    }

    /// True when the JSON value has **any** of these keys: Postgres'
    /// `?|` operator.
    fn write_json_has_any_keys(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholders: &[String],
    ) {
        write_pg_array_keys(sql, qualified_col, placeholders, " ?| ARRAY[");
    }

    /// True when the JSON value has **all** of these keys: Postgres'
    /// `?&` operator.
    fn write_json_has_all_keys(
        &self,
        sql: &mut String,
        qualified_col: &str,
        placeholders: &[String],
    ) {
        write_pg_array_keys(sql, qualified_col, placeholders, " ?& ARRAY[");
    }

    /// Append this dialect's `ON CONFLICT` clause. `model` is the insert's
    /// table: MySQL names its PK in the clause. MySQL ignores a `DoUpdate`
    /// target, since `ON DUPLICATE KEY UPDATE` fires on any unique key.
    ///
    /// # Errors
    /// [`SqlError::ConflictNotSupportedInDialect`] when the dialect
    /// cannot express the requested shape.
    fn write_conflict_clause(
        &self,
        sql: &mut String,
        model: &ModelSchema,
        conflict: &ConflictClause,
    ) -> Result<(), SqlError> {
        let _ = (sql, model);
        let shape = match conflict {
            ConflictClause::DoNothing => "DO NOTHING",
            ConflictClause::DoUpdate { .. } => "DO UPDATE",
        };
        Err(SqlError::ConflictNotSupportedInDialect {
            shape,
            dialect: self.name(),
        })
    }

    // ---- Advisory locks ----
    //
    // The migration runner keeps concurrent runs apart with two
    // locks: a session one held while applying the pending list, and
    // a transaction one held while creating the ledger table. SQLite
    // needs neither, since it has a single writer.

    /// SQL that tries a session-scoped advisory lock without waiting and
    /// returns whether it was taken. The runner polls it, so a waiting run
    /// holds no pool connection (#2027). `None` skips the lock.
    fn acquire_session_lock_sql(&self) -> Option<String> {
        None
    }

    /// SQL that releases the lock from
    /// [`Self::acquire_session_lock_sql`]. A failure here is logged,
    /// not returned: the migration's own error matters more.
    fn release_session_lock_sql(&self) -> Option<String> {
        None
    }

    /// SQL that takes a transaction-scoped advisory lock, released at
    /// COMMIT or ROLLBACK. It stops two processes from both passing
    /// `CREATE TABLE IF NOT EXISTS` for the ledger and then colliding.
    fn acquire_xact_lock_sql(&self) -> Option<String> {
        None
    }

    /// The column comment to splice into a `CREATE TABLE` column
    /// definition. Only MySQL writes one here; Postgres uses
    /// [`Self::column_comment_statement`] instead, and SQLite has no
    /// comments at all.
    ///
    /// An implementation must escape single quotes itself.
    fn write_inline_column_comment(&self, _comment: &str) -> Option<String> {
        None
    }

    /// A `COMMENT ON COLUMN` statement to run after `CREATE TABLE`.
    /// Only Postgres needs one; MySQL writes its comment inline in
    /// [`Self::write_inline_column_comment`].
    ///
    /// The runner calls this for every field that has a comment. An
    /// implementation must escape single quotes itself.
    fn column_comment_statement(
        &self,
        _table: &str,
        _column: &str,
        _comment: &str,
    ) -> Option<String> {
        None
    }

    /// The table comment to splice into the `CREATE TABLE` trailer.
    /// [`Self::write_inline_column_comment`] for the whole table.
    fn write_inline_table_comment(&self, _comment: &str) -> Option<String> {
        None
    }

    /// A `COMMENT ON TABLE` statement to run after `CREATE TABLE`.
    /// [`Self::column_comment_statement`] for the whole table.
    fn table_comment_statement(&self, _table: &str, _comment: &str) -> Option<String> {
        None
    }

    // ---- Compilation, always overridden ----

    /// Compile a `SelectQuery` for this dialect.
    ///
    /// # Errors
    /// [`SqlError`] when a filter's value does not suit its operator.
    fn compile_select(&self, query: &SelectQuery) -> Result<CompiledStatement, SqlError>;

    /// Compile an `InsertQuery` for this dialect.
    ///
    /// # Errors
    /// [`SqlError::EmptyInsert`] with no columns, or
    /// [`SqlError::InsertShapeMismatch`] when `columns` and `values`
    /// are different lengths.
    fn compile_insert(&self, query: &InsertQuery) -> Result<CompiledStatement, SqlError>;

    /// Compile a `BulkInsertQuery` into one statement with a VALUES
    /// tuple per row.
    ///
    /// # Errors
    /// [`SqlError::EmptyBulkInsert`] with no rows,
    /// [`SqlError::EmptyInsert`] when `columns` is empty and nothing
    /// is returned, or [`SqlError::InsertShapeMismatch`] when a row's
    /// length does not match `columns`.
    fn compile_bulk_insert(&self, query: &BulkInsertQuery) -> Result<CompiledStatement, SqlError>;

    /// Compile an `UpdateQuery` for this dialect.
    ///
    /// # Errors
    /// [`SqlError::EmptyUpdateSet`] when `set` is empty, or a filter
    /// error from the WHERE clause.
    fn compile_update(&self, query: &UpdateQuery) -> Result<CompiledStatement, SqlError>;

    /// Compile a `DeleteQuery` for this dialect.
    ///
    /// # Errors
    /// [`SqlError`] for a bad filter in the WHERE clause.
    fn compile_delete(&self, query: &DeleteQuery) -> Result<CompiledStatement, SqlError>;

    /// Compile a `CountQuery` into `SELECT COUNT(*) … WHERE …`.
    ///
    /// # Errors
    /// [`SqlError`] for a bad filter in the WHERE clause.
    fn compile_count(&self, query: &CountQuery) -> Result<CompiledStatement, SqlError>;

    /// Compile an `AggregateQuery` into
    /// `SELECT … GROUP BY … HAVING …`.
    ///
    /// # Errors
    /// [`SqlError`] for a bad filter in WHERE or HAVING, or for empty
    /// `aggregates`.
    fn compile_aggregate(&self, query: &AggregateQuery) -> Result<CompiledStatement, SqlError>;

    /// Compile a `BulkUpdateQuery`, updating many rows from one
    /// inline VALUES list joined on the primary key.
    ///
    /// # Errors
    /// [`SqlError::EmptyBulkInsert`] with no rows,
    /// [`SqlError::EmptyUpdateSet`] with no columns, or
    /// [`SqlError::MissingPrimaryKey`] when the model has no PK.
    fn compile_bulk_update(&self, query: &BulkUpdateQuery) -> Result<CompiledStatement, SqlError>;
}

#[cfg(test)]
mod quote_with_tests {
    #[test]
    fn doubles_every_embedded_quote() {
        assert_eq!(super::quote_with("col", '"'), "\"col\"");
        assert_eq!(super::quote_with("a\"b\"", '"'), "\"a\"\"b\"\"\"");
        assert_eq!(super::quote_with("\"", '"'), "\"\"\"\"");
        assert_eq!(super::quote_with("", '`'), "``");
    }
}

#[cfg(test)]
mod every_dialect_overrides_the_drop_constraint_methods {
    //! Catches a dialect in this crate that forgot to override one of
    //! `Dialect`'s two panicking defaults.
    //!
    //! Those defaults keep a downstream `impl Dialect` compiling, but
    //! they also mean that dropping one of these methods from a
    //! dialect here would still build, and fail instead as a panic
    //! partway through a migration.

    use super::Dialect;

    /// Every dialect answers both, one way or the other. `None` is a
    /// fine answer; inheriting the default is not.
    #[test]
    fn no_dialect_falls_through_to_the_panicking_default() {
        let dialects: Vec<&dyn Dialect> = vec![
            #[cfg(feature = "postgres")]
            &crate::sql::postgres::Postgres,
            #[cfg(feature = "mysql")]
            &crate::sql::mysql::MySql,
            #[cfg(feature = "sqlite")]
            &crate::sql::sqlite::Sqlite,
        ];

        assert!(
            !dialects.is_empty(),
            "no dialect features are on, so this guard checked nothing"
        );

        for d in dialects {
            // The default body panics; any return means the dialect
            // answered for itself.
            let _ = d.drop_check_constraint_sql("t", "c");
            let _ = d.drop_foreign_key_sql("t", "fk");
        }
    }
}