cratefield-import-supabase 0.2.3

Read-only inspection of a Supabase project and the migration report the Supabase importer is planned from (ADR 0026)
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
//! The migration report: what inspect saw, and what each item needs.
//!
//! The JSON rendering of [`Report`] is the contract the later importer
//! steps (#659, #660, #661) and the dashboard read, documented field by
//! field in `docs/import/supabase-report.md`. Its rules:
//!
//! - [`REPORT_VERSION`] changes whenever a field is removed, renamed or
//!   changes meaning. Adding a field does not change it; a reader ignores
//!   fields it does not know.
//! - Every list is sorted (by schema, then name, then whatever else makes
//!   the key unique), so two inspections of the same project render the
//!   same bytes. There is no timestamp in the report.
//! - Nothing secret is in it: no credential, no password hash, no email,
//!   no row data. The connection is named by host, port and database only,
//!   and every free-text SQL fragment has passed through `scrub_text`.

use serde::{Deserialize, Serialize};

/// The version of the report's JSON shape. See the module docs for when it
/// changes. 2: the row counts a role cannot see are `null`, not `0`, and
/// `storage.counts_exact` is gone (issue #723); `coverage.sections` says
/// which section was invisible and why.
pub const REPORT_VERSION: u32 = 2;

/// The whole report. Field order is the JSON's key order.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Report {
    /// [`REPORT_VERSION`] at the time this report was written.
    pub report_version: u32,
    /// Which tool wrote it.
    pub tool: Tool,
    /// The project, named without credentials.
    pub project: Project,
    /// The evidence that the inspection could not write.
    pub read_only: ReadOnlyEvidence,
    /// What was inspected and what could not be.
    pub coverage: Coverage,
    /// Totals, counts per classification and the transfer estimate.
    pub summary: Summary,
    /// The dispositions of the needs-work and blocker items.
    pub dispositions: DispositionsReport,
    /// Every non-system schema, Supabase-managed ones included.
    pub schemas: Vec<Schema>,
    /// Tables in the user schemas (never `auth`, `storage` or another
    /// Supabase-managed schema: those have their own sections).
    pub tables: Vec<Table>,
    /// Views and materialized views in the user schemas.
    pub views: Vec<View>,
    /// Sequences in the user schemas.
    pub sequences: Vec<Sequence>,
    /// Enum types in the user schemas.
    pub enums: Vec<EnumType>,
    /// Every installed extension, with its support status.
    pub extensions: Vec<Extension>,
    /// Functions in the user schemas, excluding extension members.
    pub functions: Vec<Function>,
    /// Triggers on user tables, and user-defined triggers on `auth` and
    /// `storage` tables.
    pub triggers: Vec<Trigger>,
    /// Every row-level-security policy outside the system catalogs, except
    /// the managed ones and the two storage tables (below).
    pub policies: Vec<Policy>,
    /// Policies on Supabase-managed schemas (`auth`, `cron`, `vault`, and
    /// every `storage` table other than `objects`/`buckets`): the platform's,
    /// not the venture's, so they are listed for audit only — never
    /// classified, never a finding.
    pub managed_policies: Vec<ManagedPolicy>,
    /// Table grants to Supabase's API roles, one entry per role.
    pub api_role_grants: Vec<RoleGrants>,
    /// Supabase Auth: counts and providers, never a user.
    pub auth: Auth,
    /// Supabase Storage buckets.
    pub storage: Storage,
    /// Edge Functions (Management API only).
    pub edge_functions: EdgeFunctions,
    /// Logical-replication publications, which is what Realtime reads.
    pub realtime: Realtime,
    /// `pg_cron` jobs; `null` when `cron.job` is not visible to the role,
    /// `[]` when there is no `cron.job` (or no jobs). Never `[]` when the
    /// jobs are merely hidden.
    pub cron_jobs: Option<Vec<CronJob>>,
    /// Every item classified: automatic, needs work, or a blocker.
    pub findings: Vec<Finding>,
    /// Things the reader should know that are not findings: a role that
    /// could write, counts RLS may have hidden, a Management API call that
    /// failed.
    pub warnings: Vec<String>,
}

/// The tool that wrote a report.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tool {
    /// The crate name.
    pub name: String,
    /// The crate version.
    pub version: String,
}

/// The project, named without credentials.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Project {
    /// The Supabase project ref.
    #[serde(rename = "ref")]
    pub project_ref: String,
    /// The database host. Never a user name or password.
    pub host: String,
    /// The database port.
    pub port: u16,
    /// The database name.
    pub database: String,
    /// `server_version` as the server reports it.
    pub server_version: String,
}

/// How the inspection was kept from writing, as observed — not as
/// intended.
// Independent observations, each its own yes or no in the JSON.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadOnlyEvidence {
    /// The reads ran in a `READ ONLY` transaction
    /// (`transaction_read_only` read back inside it).
    pub transaction_read_only: bool,
    /// The session default was read-only too
    /// (`default_transaction_read_only`).
    pub session_read_only: bool,
    /// No transaction id was ever assigned (`txid_current_if_assigned()`
    /// was null at the end), which a write would have forced.
    pub no_transaction_id_assigned: bool,
    /// The role inspect connected as.
    pub role: String,
    /// The role is a superuser.
    pub role_is_superuser: bool,
    /// The role holds a write privilege — `INSERT`, `UPDATE`, `DELETE`,
    /// `TRUNCATE` on a user table, or `CREATE` on a user schema. The
    /// documented read-only role has none.
    pub role_can_write: bool,
}

/// What each source of facts contributed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Coverage {
    /// Always `inspected`: without the database there is no report.
    pub database: SourceStatus,
    /// The Supabase Management API: `not_inspected` without a token.
    pub management_api: SourceStatus,
    /// The RLS classifier: `not_inspected` unless one was configured.
    pub policy_classifier: SourceStatus,
    /// One entry per section whose rows the role could not fully read,
    /// plus one for each section it could: `auth`, `storage.buckets`,
    /// `storage.objects`, `cron` (only when `cron.job` exists) and
    /// `schema:<name>` for each user schema. A `not_visible` section's data
    /// is `null`, never `0`, and there is a blocker finding with the SQL
    /// that fixes it.
    pub sections: Vec<SectionCoverage>,
}

/// One section of the report and what the inspecting role could see of it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SectionCoverage {
    /// `auth`, `storage.buckets`, `storage.objects`, `cron`, or
    /// `schema:<name>`.
    pub section: String,
    /// `inspected` or `not_visible` (unknown, never "none").
    pub coverage: SourceStatus,
    /// Why the section is not visible; empty, and omitted from the JSON,
    /// when it is.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub reason: String,
    /// The SQL that would make the section visible, when there is one;
    /// empty, and omitted from the JSON, otherwise.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub fix: String,
}

/// Whether a source was read.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceStatus {
    /// Read in full.
    Inspected,
    /// Not read, because nothing to read it with was given. Absence of
    /// data from it means "unknown", never "none".
    NotInspected,
    /// The role lacks a privilege a row read needs, so the section is
    /// unknown, never "none".
    NotVisible,
    /// Tried and failed; the warnings say why.
    Failed,
}

/// Totals and the rough transfer estimate.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Summary {
    /// Findings classified automatic.
    pub automatic: usize,
    /// Findings classified needs work.
    pub needs_work: usize,
    /// Findings classified blocker.
    pub blockers: usize,
    /// Needs-work and blocker items with a `covered` or `waived`
    /// disposition.
    pub decided: usize,
    /// Needs-work and blocker items with no disposition yet. Cutover
    /// refuses while this is non-zero (#661).
    pub undecided: usize,
    /// No blockers: the later steps can run once the needs-work items have
    /// a disposition.
    pub ready: bool,
    /// User tables.
    pub tables: usize,
    /// Sum of the tables' planner row estimates.
    pub estimated_rows: u64,
    /// Heap and TOAST bytes of the user tables — what the data phase moves.
    pub data_bytes: u64,
    /// Index bytes of the user tables, rebuilt on the target rather than
    /// moved.
    pub index_bytes: u64,
    /// Storage objects, summed over the buckets; `null` when the buckets
    /// or their object counts are not visible to the role.
    pub storage_objects: Option<u64>,
    /// Storage bytes, from each object's recorded size; `null` when the
    /// buckets or their object counts are not visible to the role.
    pub storage_bytes: Option<u64>,
    /// The throughput the estimate assumes, in megabits per second.
    pub transfer_assumed_mbps: u32,
    /// `(data_bytes + storage_bytes)` at that throughput, rounded up, plus
    /// nothing else: index builds, validation and verification are not in
    /// it. A rough guide, not a promise.
    pub estimated_transfer_seconds: u64,
}

/// A schema and whether it is the user's.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Schema {
    /// The schema name.
    pub name: String,
    /// `user` or `supabase_managed`.
    pub kind: SchemaKind,
    /// Where the importer puts it: `app` for `public`, the same name for
    /// another user schema, `null` for a Supabase-managed one.
    pub target: Option<String>,
    /// Ordinary and partitioned tables in it.
    pub tables: usize,
}

/// Whose schema it is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SchemaKind {
    /// The project's own; copied.
    User,
    /// Supabase's (auth, storage, realtime, extensions, …); never copied
    /// as a schema.
    SupabaseManaged,
}

/// A user table.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Table {
    /// Its schema.
    pub schema: String,
    /// Its name.
    pub name: String,
    /// `table` or `partitioned_table`.
    pub kind: String,
    /// The planner's estimate (`pg_class.reltuples`); `null` when the
    /// table has never been analyzed.
    pub estimated_rows: Option<u64>,
    /// Heap and TOAST bytes.
    pub data_bytes: u64,
    /// Index bytes.
    pub index_bytes: u64,
    /// Row-level security is enabled.
    pub rls_enabled: bool,
    /// Row-level security is forced on the owner too.
    pub rls_forced: bool,
    /// The primary key's columns, in key order; empty when it has none.
    pub primary_key: Vec<String>,
    /// Columns in ordinal order.
    pub columns: Vec<Column>,
    /// Constraints, by name.
    pub constraints: Vec<Constraint>,
    /// Indexes, by name.
    pub indexes: Vec<Index>,
}

/// A column.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Column {
    /// Its name.
    pub name: String,
    /// `format_type` of its type.
    pub data_type: String,
    /// It accepts null.
    pub nullable: bool,
    /// Its default expression, scrubbed.
    pub default: Option<String>,
    /// `always` or `by_default` for an identity column.
    pub identity: Option<String>,
    /// The expression of a stored generated column, scrubbed.
    pub generated: Option<String>,
}

/// A table constraint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Constraint {
    /// Its name.
    pub name: String,
    /// `primary_key`, `foreign_key`, `unique`, `check`, `exclusion` or
    /// `trigger`.
    pub kind: String,
    /// `pg_get_constraintdef`, scrubbed.
    pub definition: String,
    /// The referenced table of a foreign key, as `schema.table`.
    pub references: Option<String>,
}

/// An index.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Index {
    /// Its name.
    pub name: String,
    /// It is unique.
    pub unique: bool,
    /// It backs the primary key.
    pub primary: bool,
    /// `pg_get_indexdef`.
    pub definition: String,
}

/// A view or materialized view.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct View {
    /// Its schema.
    pub schema: String,
    /// Its name.
    pub name: String,
    /// It is a materialized view.
    pub materialized: bool,
    /// Its definition names `auth.`.
    pub references_auth: bool,
    /// Its definition names `storage.`.
    pub references_storage: bool,
}

/// A sequence.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Sequence {
    /// Its schema.
    pub schema: String,
    /// Its name.
    pub name: String,
    /// `table.column` of the column that owns it, if one does.
    pub owned_by: Option<String>,
    /// Its data type.
    pub data_type: String,
}

/// An enum type.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnumType {
    /// Its schema.
    pub schema: String,
    /// Its name.
    pub name: String,
    /// Its labels, in sort order.
    pub labels: Vec<String>,
}

/// An installed extension.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Extension {
    /// Its name.
    pub name: String,
    /// The installed version.
    pub version: String,
    /// The schema it is installed in.
    pub schema: String,
    /// What the harness can do with it.
    pub support: ExtensionSupport,
}

/// What the harness Postgres target can do with an extension.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExtensionSupport {
    /// Created on the target before the schema; nothing to do.
    Supported,
    /// Part of the Supabase platform; not carried over. What used it
    /// needs a harness equivalent.
    SupabasePlatform,
    /// No harness equivalent and no way to carry it: a blocker.
    Unsupported,
    /// Not on the support list: the target may or may not have it.
    Unknown,
}

/// A user function.
// Independent facts about one function, each its own field in the JSON.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Function {
    /// Its schema.
    pub schema: String,
    /// Its name.
    pub name: String,
    /// `pg_get_function_identity_arguments`.
    pub arguments: String,
    /// `function`, `procedure`, `aggregate` or `window`.
    pub kind: String,
    /// Its language.
    pub language: String,
    /// Its result type.
    pub returns: String,
    /// It runs as its owner.
    pub security_definer: bool,
    /// Its body names `auth.`.
    pub references_auth: bool,
    /// Its body names `storage.`.
    pub references_storage: bool,
    /// Its body calls `pg_net` (`net.http_…`).
    pub references_net: bool,
}

/// A trigger.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Trigger {
    /// The schema of its table.
    pub schema: String,
    /// Its table.
    pub table: String,
    /// Its name.
    pub name: String,
    /// The function it runs, as `schema.name`.
    pub function: String,
    /// `pg_get_triggerdef`.
    pub definition: String,
    /// It is enabled.
    pub enabled: bool,
}

/// A row-level-security policy, with what inspect made of it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Policy {
    /// The schema of its table.
    pub schema: String,
    /// Its table.
    pub table: String,
    /// Its name.
    pub name: String,
    /// `ALL`, `SELECT`, `INSERT`, `UPDATE` or `DELETE`.
    pub command: String,
    /// Permissive (true) or restrictive.
    pub permissive: bool,
    /// The roles it applies to, sorted.
    pub roles: Vec<String>,
    /// The `USING` expression, scrubbed.
    pub using: Option<String>,
    /// The `WITH CHECK` expression, scrubbed.
    pub with_check: Option<String>,
    /// What the policy amounts to.
    pub pattern: PolicyPattern,
    /// How sure: 1.0 for a rule match, the classifier's confidence for a
    /// classifier answer, 0.0 for an unplaced policy.
    pub confidence: f32,
    /// Who placed it.
    pub source: PolicySource,
    /// The label the classifier answered when it was below the threshold
    /// (the policy is then `needs_review`); `null` otherwise.
    pub classifier_label: Option<String>,
    /// The check to write in code, as advice: ADR 0026 rejects
    /// translating RLS automatically, so this is never enforcement code.
    pub suggested_equivalent: String,
    /// A failing (`todo!()`) Rust test stub naming the policy and its
    /// expression, for the venture to turn into the test that proves its
    /// replacement check. Written into the run directory, never into the
    /// venture's source tree.
    pub test_stub: String,
    /// `covered`, `waived` or `undecided`: this item's entry in the
    /// dispositions file (`--dispositions`), and `undecided` when no file
    /// covers it. Before cutover each policy needs its own `covered` (with a
    /// reference to the code or test) or `waived` (with a reason) — never in
    /// bulk (ADR 0026, Decision 5).
    pub disposition: Disposition,
}

/// A policy on a Supabase-managed schema (`auth`, `cron`, `vault`, …): the
/// platform's, not the venture's — audited, never classified, copied or a
/// finding.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManagedPolicy {
    /// The schema of its table.
    pub schema: String,
    /// Its table.
    pub table: String,
    /// Its name.
    pub name: String,
    /// `ALL`, `SELECT`, `INSERT`, `UPDATE` or `DELETE`.
    pub command: String,
    /// The roles it applies to, sorted.
    pub roles: Vec<String>,
    /// The `USING` expression, scrubbed.
    pub using: Option<String>,
    /// The `WITH CHECK` expression, scrubbed.
    pub with_check: Option<String>,
}

/// An RLS policy on `storage.objects`/`storage.buckets`, attached to the
/// bucket(s) it governs; each needs its own disposition (ADR 0026,
/// Decision 5).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BucketPolicy {
    /// `storage.objects` or `storage.buckets`.
    pub table: String,
    /// Its name.
    pub name: String,
    /// `ALL`, `SELECT`, `INSERT`, `UPDATE` or `DELETE`.
    pub command: String,
    /// The roles it applies to, sorted.
    pub roles: Vec<String>,
    /// The `USING` expression, scrubbed.
    pub using: Option<String>,
    /// The `WITH CHECK` expression, scrubbed.
    pub with_check: Option<String>,
    /// The expression named no bucket that exists, so the policy applies to
    /// every bucket (and to any added later).
    pub all_buckets: bool,
    /// The storage-specific check to write, as advice — never enforcement
    /// code.
    pub suggested_equivalent: String,
    /// Always `undecided` in an inspect report.
    pub disposition: Disposition,
}

/// The decision a reported item needs before cutover.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Disposition {
    /// Nobody has decided yet: cutover refuses.
    Undecided,
    /// Code and a test now do what the item did.
    Covered,
    /// Deliberately not replaced, with a reason.
    Waived,
}

/// The access pattern a policy expresses.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyPattern {
    /// A row belongs to one user: `auth.uid() = owner_column`.
    OwnerOnly,
    /// A row belongs to a team, organisation or tenant the user is a
    /// member of, through a membership lookup.
    TenantScoped,
    /// Everyone may read: `USING (true)` on a read.
    PublicRead,
    /// Everyone may write: `true` on a write.
    PublicWrite,
    /// A role or JWT claim decides: `auth.role()`, `auth.jwt()`, or `true`
    /// for `authenticated` only.
    RoleBased,
    /// Only the service role: no client ever passes it.
    ServiceRoleOnly,
    /// Ownership through a parent row: the row's foreign key points at a
    /// parent whose owner column must equal the request's subject.
    OwnerViaParent,
    /// Membership through a parent row: the row's foreign key points at a
    /// parent joined to a membership table the subject appears in.
    TenantViaParent,
    /// A public read bounded by a filter on the row's own columns (or on a
    /// parent row reached through a foreign key): no subject check.
    PublicReadFiltered,
    /// The policy refuses everything (`false` in `USING` and/or `WITH
    /// CHECK`).
    DenyAll,
    /// A disjunction of patterns: any one of them grants access.
    Composite,
    /// The classifier judged it bespoke logic.
    CustomLogic,
    /// Not placed by a rule, and either no classifier was configured or
    /// its answer was below the threshold. A person reads it.
    NeedsReview,
}

/// Who placed a policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicySource {
    /// A deterministic rule (or no rule matched and no classifier ran).
    Rule,
    /// The configured `Classifier`.
    Classifier,
}

/// Table privileges granted to one of Supabase's API roles.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoleGrants {
    /// `anon`, `authenticated` or `service_role`.
    pub role: String,
    /// `schema.table` of every user table it holds a privilege on.
    pub tables: Vec<String>,
}

/// Supabase Auth, as counts. A count is `null` when the role cannot see
/// `auth.users`/`auth.identities`; unknown, never zero.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Auth {
    /// The `auth` schema exists.
    pub present: bool,
    /// Rows in `auth.users`.
    pub users: Option<u64>,
    /// Users with no password (OAuth, magic link or phone only).
    pub users_without_password: Option<u64>,
    /// Users whose email is not confirmed.
    pub users_unconfirmed: Option<u64>,
    /// Anonymous users (`is_anonymous`), when the column exists.
    pub anonymous_users: Option<u64>,
    /// `auth.identities` rows per provider, sorted by provider.
    pub identities_by_provider: Option<Vec<ProviderCount>>,
    /// MFA factors enrolled (`auth.mfa_factors`), when the table exists.
    pub mfa_factors: Option<u64>,
    /// SSO providers (`auth.sso_providers`), when the table exists.
    pub sso_providers: Option<u64>,
    /// The providers the project's auth configuration enables, from the
    /// Management API. `null` when it was not inspected.
    pub enabled_providers: Option<Vec<String>>,
    /// MFA methods the configuration enables, from the Management API.
    /// `null` when it was not inspected.
    pub enabled_mfa: Option<Vec<String>>,
}

/// Identities of one provider.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderCount {
    /// The provider name (`email`, `google`, `github`, …).
    pub provider: String,
    /// Identities with it.
    pub identities: u64,
}

/// Supabase Storage.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Storage {
    /// The `storage` schema exists.
    pub present: bool,
    /// Buckets, by id; `null` when `storage.buckets` is not visible to the
    /// role. Never `[]` on an invisible section.
    pub buckets: Option<Vec<Bucket>>,
    /// Policies on `storage.objects`/`storage.buckets` that could not be
    /// attached to any bucket: the project has no buckets at all, or the
    /// bucket list is not visible to the role. Empty otherwise.
    pub unattached_policies: Vec<BucketPolicy>,
}

/// A bucket.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bucket {
    /// Its id.
    pub id: String,
    /// Its name.
    pub name: String,
    /// Objects are readable without a token.
    pub public: bool,
    /// The per-object size limit, in bytes.
    pub file_size_limit: Option<u64>,
    /// The allowed MIME types, sorted.
    pub allowed_mime_types: Vec<String>,
    /// Objects in it; `null` when `storage.objects` is not visible.
    pub objects: Option<u64>,
    /// The sum of their recorded sizes; `null` when `storage.objects` is
    /// not visible.
    pub bytes: Option<u64>,
    /// Objects over the Blob port's 10 MiB put cap; `null` when
    /// `storage.objects` is not visible.
    pub objects_over_blob_cap: Option<u64>,
    /// `storage.objects`/`storage.buckets` policies that name this bucket
    /// (or, with `all_buckets`, apply to every bucket).
    pub policies: Vec<BucketPolicy>,
}

/// Edge Functions.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EdgeFunctions {
    /// Whether the list was read; `not_inspected` means "unknown", never
    /// "none".
    pub status: SourceStatus,
    /// The functions, by slug.
    pub functions: Vec<EdgeFunction>,
}

/// An Edge Function.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EdgeFunction {
    /// Its slug.
    pub slug: String,
    /// Its name.
    pub name: String,
    /// `ACTIVE`, `REMOVED` or `THROTTLED`.
    pub status: String,
    /// It requires a Supabase JWT.
    pub verify_jwt: Option<bool>,
}

/// Realtime publications.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Realtime {
    /// Publications, by name.
    pub publications: Vec<Publication>,
}

/// A publication.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Publication {
    /// Its name (`supabase_realtime` is Realtime's).
    pub name: String,
    /// It publishes every table.
    pub all_tables: bool,
    /// The operations it publishes, sorted.
    pub operations: Vec<String>,
    /// `schema.table` of every table it publishes, sorted.
    pub tables: Vec<String>,
}

/// A `pg_cron` job.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CronJob {
    /// Its name, or `job-<id>` when it has none.
    pub name: String,
    /// Its cron schedule.
    pub schedule: String,
    /// Its command, scrubbed.
    pub command: String,
    /// It is active.
    pub active: bool,
}

/// One classified item.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
    /// A stable id: `<kind>:<qualified name>`. Sorting is by this id.
    pub id: String,
    /// What kind of object it is (`table`, `policy`, `extension`, …).
    pub kind: String,
    /// The object's qualified name.
    pub object: String,
    /// What the importer can do with it.
    pub classification: Classification,
    /// The importer phase that handles it (`schema`, `data`, `auth`,
    /// `storage`) or `code` when the venture's own code has to.
    pub phase: String,
    /// Why it is classified so.
    pub reason: String,
    /// What it becomes on Cratefield.
    pub cratefield_equivalent: String,
}

/// What the importer can do with an item.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Classification {
    /// Moved by the importer with nothing to decide.
    Automatic,
    /// Moved or reported, but the venture has to write or decide
    /// something: each needs a disposition before cutover (ADR 0026,
    /// Decision 5).
    NeedsWork,
    /// The importer cannot proceed until it is resolved.
    Blocker,
}

impl Classification {
    /// The label the Markdown report uses.
    #[must_use]
    pub fn label(self) -> &'static str {
        match self {
            Self::Automatic => "automatic",
            Self::NeedsWork => "needs work",
            Self::Blocker => "blocker",
        }
    }
}

/// The dispositions in a report, per kind and item.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DispositionsReport {
    /// Counts per kind of item that needs a disposition, only kinds with
    /// at least one item, sorted by kind.
    pub by_kind: Vec<KindDispositions>,
    /// Every item decided, sorted by id.
    pub decided: Vec<DecidedItem>,
    /// Entries in the file that match no item that needs a disposition,
    /// sorted.
    pub stale: Vec<String>,
}

/// The disposition counts of one kind of item.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KindDispositions {
    /// The finding kind (`policy`, `function`, `extension`, …).
    pub kind: String,
    /// Items of this kind covered.
    pub covered: usize,
    /// Items of this kind waived.
    pub waived: usize,
    /// Items of this kind still undecided.
    pub undecided: usize,
}

/// One item with a disposition.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DecidedItem {
    /// The finding id.
    pub id: String,
    /// `covered` or `waived`.
    pub status: Disposition,
    /// The code or test that covers it; `covered` entries only.
    #[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,
    /// Why it is waived; `waived` entries only.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

impl Report {
    /// The report as pretty-printed JSON, with a trailing newline.
    ///
    /// # Panics
    ///
    /// Never: every field serializes.
    #[must_use]
    pub fn to_json(&self) -> String {
        let mut out = serde_json::to_string_pretty(self).expect("a report always serializes");
        out.push('\n');
        out
    }
}