razeri-plugin-contract 0.18.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
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
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! Replication rendering traits with ANSI defaults.
//!
//! [`ReplicationRenderer`] extends [`SqlRenderer`] with methods for
//! SID (surrogate ID) function generation, data extraction, data loading, and
//! checksum validation. Each dialect crate overrides these methods to produce
//! platform-native SQL.
//!
//! ## Format names
//!
//! Methods that accept a `format` parameter use well-known string identifiers:
//!
//! | Name | Description |
//! |------|-------------|
//! | `"csv"` | Comma-separated values |
//! | `"parquet"` | Apache Parquet columnar format |
//! | `"json_lines"` | JSON Lines (one object per line) |
//! | `"native"` | Platform-native binary format |

use crate::SqlRenderer;

// ════════════════════════════════════════════════════════════════════════
// SID Prerequisites — the statement boundary
// ════════════════════════════════════════════════════════════════════════

/// **The line a dialect writes between two independently-executable statements
/// inside [`ReplicationRenderer::sid_prerequisites`].**
///
/// A prerequisite script is mostly prose — the `MariaDB` C source and the SQL
/// Server C# are emitted as comment blocks for an operator to compile — but two
/// arms emit real DDL a machine must run one statement at a time: Snowflake's
/// JavaScript UDF plus its SQL wrapper, and Databricks' Python UDF. Neither can
/// be recovered by splitting on `;`, because **both bodies contain
/// semicolons**: a JavaScript function body is nothing but semicolons.
///
/// So the boundary is declared rather than inferred. The delimiter is a SQL line
/// comment, so a script carrying it is still one paste-able file for a human
/// operator, and [`ReplicationRenderer::sid_prerequisite_statements`] is the one
/// splitter — a consumer that re-derived the boundary would be a second
/// definition of it, and a second definition is how the two drift apart. Do not
/// write your own splitter; split on this constant or call the trait method.
pub const SID_PREREQUISITE_STATEMENT_DELIMITER: &str =
    "-- ──── RAZERI SID PREREQUISITE STATEMENT ────";

/// **The input every generated prerequisite probe asks about.**
///
/// One word, and it is the one [`ReplicationRenderer::sid_validation_query`]
/// already puts in its `SELECT`. It lives here because three producers must
/// agree on it and they live in different crates: the dialect writing the
/// operator's instructions, the generator writing the deploy script's
/// comparison, and the validation query itself. A probe that asked about one
/// word while the instructions promised another would be a second, hand-spelled
/// variant of the same fact — the kind of duplicate that drifts unnoticed.
pub const SID_PROBE_INPUT: &str = "test";

// ════════════════════════════════════════════════════════════════════════
// SID Prerequisites — where the helper they install lives
// ════════════════════════════════════════════════════════════════════════

/// **The schema Razeri keeps its own engine-level objects in**, where an engine
/// has schemas and the dialect asks for one.
///
/// One word, and it is Razeri's rather than the design's. The hash a key
/// function calls is installed once for the whole engine, and a design has as
/// many schemas as it has domains: putting the hash in one of them would make
/// every other domain depend on that domain's schema, and domains are meant to
/// be deployable apart. So the helper gets a home of its own, named here so
/// that a dialect, a deploy script and a test all read the same word.
pub const RAZERI_HELPER_SCHEMA: &str = "razeri";

/// **Where the objects a prerequisite installs actually live**, so that
/// something calling them from another schema can name them.
///
/// Most engines need no answer. Their prerequisite installs nothing with a
/// name — a session setting, an extension registered for the whole database, a
/// server-global function — or installs the key function itself, which Razeri
/// already creates and calls in a known schema. Those say [`Ambient`] and their
/// scripts are what they were, byte for byte.
///
/// [`Schema`] is for an engine where a routine resolves an unqualified name in
/// the schema that OWNS the routine rather than in the session's. There, a hash
/// installed wherever the deploy happened to sign in is invisible to every key
/// function created in a design's own schema — measured on Oracle as
/// `ORA-00904: "RAZERI_XXH64": invalid identifier`, once, with every function
/// beneath it then `ORA-06575 … in an invalid state`. Naming the home fixes the
/// helper in one place and lets every call site spell it.
///
/// [`Ambient`]: Self::Ambient
/// [`Schema`]: Self::Schema
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SidPrerequisiteHome {
    /// **Wherever the deploy runs** — the default, and the honest answer for an
    /// engine with no schema for a function at all.
    ///
    /// `SQLite` loads a library into each connection; `MySQL` and `MariaDB`
    /// register a library for the whole server; `ClickHouse`'s `CREATE FUNCTION` takes no
    /// qualifier; `DuckDB` loads an extension into the session. None of them
    /// has a schema to put a helper in, and a call site that invented one would
    /// name an object that does not exist.
    Ambient,

    /// **A schema Razeri owns**, named here, which the prerequisite creates and
    /// installs into and which every call site then qualifies with.
    ///
    /// Use [`RAZERI_HELPER_SCHEMA`] unless your engine reserves that word: one
    /// home for one engine-level helper is the point, and two dialects naming
    /// two homes would be two places to look.
    ///
    /// **What answering this promises.** The prerequisite's own text creates
    /// its objects IN that schema, and makes them callable from outside it —
    /// on an engine with privileges, that means the grant travels with the
    /// install. Razeri writes the schema's own creation in front of the
    /// install, in whatever this engine's `CREATE SCHEMA` spelling is, and
    /// qualifies the hash in every key function it composes.
    Schema(&'static str),
}

impl SidPrerequisiteHome {
    /// The schema this home names, or `None` where there is none to name.
    #[must_use]
    pub const fn schema(self) -> Option<&'static str> {
        match self {
            Self::Ambient => None,
            Self::Schema(schema) => Some(schema),
        }
    }
}

// ════════════════════════════════════════════════════════════════════════
// SID Prerequisites — what KIND of step this platform's is
// ════════════════════════════════════════════════════════════════════════

/// **What a platform's SID prerequisite actually asks of whoever deploys.**
///
/// The bundle's prerequisites step has to emit a different *shape* for each of
/// these, and the deploy script a different *behaviour*: one runs SQL, one
/// verifies and refuses, one refuses outright, one says there is nothing to do.
/// Sniffing that out of the prose at each call site would put the rule in the
/// generator, the script writer and the runner — three readings of one fact,
/// which is exactly how one derivation becomes two.
///
/// The default derives it from what the dialect already says, so a dialect that
/// overrides nothing is still classified correctly and only a platform the
/// derivation cannot reach ([`Refused`](Self::Refused), [`None`](Self::None))
/// has to override.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SidPrerequisiteKind {
    /// **Nothing to install: the engine's own hash already IS the canonical
    /// number** (XXH64 seed 0), reachable from SQL razeri emits.
    ///
    /// **A positive claim, and therefore never a default.** Saying this means
    /// razeri has *measured* that the platform's builtin returns XXH64 seed 0 —
    /// not that nobody has said otherwise. A dialect that has declared nothing
    /// gets [`Undeclared`](Self::Undeclared) instead, for the same reason
    /// `DdlRenderer::representable_timestamp_ceiling`'s `None` means *no
    /// measured ceiling* rather than *holds everything*.
    ///
    /// The derivation cannot reach this variant. A dialect claiming it
    /// **overrides [`ReplicationRenderer::sid_prerequisite_kind`] and cites the
    /// measurement**, as Razeri's own `ClickHouse` dialect does.
    None,

    /// Razeri emits statements that install it, and a deploy script can run
    /// them unattended: `INSTALL`/`LOAD` (`DuckDB`), `CREATE FUNCTION`
    /// (Snowflake's JavaScript UDF, Databricks' Python one).
    ///
    /// The test is *"can a script finish this with no human on the host"*. It is
    /// **not** "is the statement SQL": `CREATE EXTENSION` is SQL and only
    /// **registers** a shared object somebody compiled first, which is why
    /// `PostgreSQL` and Citus are [`OperatorArtifact`](Self::OperatorArtifact)
    /// and not this.
    SqlInstallable,

    /// An artifact the **operator** builds and places before any SQL can
    /// reference it — SQL Server's CLR assembly, `MariaDB`'s and `MySQL`'s
    /// `.so`, `PostgreSQL`'s and Citus's `pg_xxhash`. Razeri emits the source
    /// or the build recipe as prose; it cannot compile anything, and it will not
    /// pretend to.
    ///
    /// The deploy path's obligation here is the prerequisites verb: **verify,
    /// then
    /// refuse** — ask the engine for the function and stop in front of `schema`
    /// when it is not there, rather than deploying a warehouse whose keys it
    /// cannot compute.
    OperatorArtifact,

    /// The platform cannot be made to compute razeri's key from SQL **at all**,
    /// and no artifact an operator drops in changes that.
    ///
    /// Redshift is the case: its `prerequisites.sql` says why. A platform with
    /// no `CREATE FUNCTION` whose client can load a compiled library is not
    /// this case — `SQLite` ships its key function as a library the operator
    /// compiles, an [`OperatorArtifact`](Self::OperatorArtifact).
    ///
    /// The answer is a **named refusal carried in the bundle**, checked before
    /// any data moves. Never a silent fall-back expression: a number that looks
    /// like a key and is not razeri's writes keys no other deployment can
    /// reproduce.
    Refused,

    /// **Nothing to install here, because razeri's own key function travels in
    /// `schema.sql` instead** — one definition in every namespace whose
    /// objects call it, created beside those objects, so a namespace deploys
    /// on its own.
    ///
    /// `BigQuery` is the case and the reason is name resolution, not taste: a
    /// bare `generate_sid('test')` is *"Function not found: `generate_sid`; Did
    /// you mean `generate_uuid`?"* there — no search path, no session schema, no
    /// default dataset inside a procedure body — so every call site must name
    /// the dataset the function was created in, and a prerequisites script has
    /// no dataset to create it in. The definition therefore rides `schema.sql`,
    /// where the domain's namespace is known.
    ///
    /// It is **not** [`None`](Self::None). That variant is the positive claim
    /// *"the engine's own hash IS the canonical number"*, which is false here:
    /// `BigQuery` has no XXH64 in any spelling and the key is a razeri-owned
    /// JavaScript UDF. What the two share is only the deploy behaviour —
    /// nothing to run before `schema.sql` — and sharing a behaviour is not
    /// sharing a claim.
    EmittedInSchema,

    /// **Razeri has no measured answer for this platform** — the honest default,
    /// and the one a dialect gets by saying nothing.
    ///
    /// # Why this variant exists
    ///
    /// A derivation that answered [`None`](Self::None) here would turn *"this
    /// dialect declared nothing"* into the positive claim *"its own hash is
    /// razeri's number"* — a claim that has been false on real engines. Silence
    /// and safety must never read the same, so silence gets its own variant.
    ///
    /// # What it means to a deploy path
    ///
    /// The same as [`OperatorArtifact`](Self::OperatorArtifact): **verify, then
    /// refuse.** Safe-by-default is the only defensible reading of "razeri does
    /// not know" — the contract allows an emitted UDF or a named refusal and
    /// explicitly not a local approximation, and proceeding on an unmeasured
    /// platform is neither.
    ///
    /// A dialect that genuinely needs no prerequisite says so by overriding
    /// [`ReplicationRenderer::sid_prerequisite_kind`] to
    /// [`None`](Self::None), with its measurement cited.
    Undeclared,
}

/// **The executable statements inside a prerequisite script**, split on
/// [`SID_PREREQUISITE_STATEMENT_DELIMITER`].
///
/// The body of [`ReplicationRenderer::sid_prerequisite_statements`], lifted out
/// so a caller holding a script that is not `self.sid_prerequisites()` — the
/// bundle, when it swaps in
/// [`ReplicationRenderer::sid_pure_sql_fallback`] — splits it the same way
/// rather than growing a second reading of the boundary.
///
/// Leading comment and blank lines are dropped from each segment and one
/// trailing terminator is stripped; see the trait method for why both.
#[must_use]
pub fn statements_in_prerequisite_script(script: &str) -> Vec<String> {
    if !script.contains(SID_PREREQUISITE_STATEMENT_DELIMITER) {
        return Vec::new();
    }
    script
        .split(SID_PREREQUISITE_STATEMENT_DELIMITER)
        .filter_map(|segment| {
            let statement = segment
                .lines()
                .skip_while(|line| {
                    let line = line.trim_start();
                    line.is_empty() || line.starts_with("--")
                })
                .collect::<Vec<_>>()
                .join("\n");
            // One trailing terminator, not a run of them: `$$;` ends a body
            // whose own last character may be a `;` the statement needs.
            let statement = statement.trim_end().strip_suffix(';').map_or_else(
                || statement.trim_end().to_owned(),
                |trimmed| trimmed.trim_end().to_owned(),
            );
            (!statement.is_empty()).then_some(statement)
        })
        .collect()
}

/// **What shape a prerequisite script is**, read off the script itself.
///
/// The body of [`ReplicationRenderer::sid_prerequisite_kind`]'s default, lifted
/// out for the same reason as [`statements_in_prerequisite_script`]: a caller
/// classifying a script the dialect did not return from `sid_prerequisites()`
/// must reach the same three answers by the same rule. A second reading of the
/// table in the trait method's own doc comment is how a script comes to be
/// classified two ways.
///
/// The two answers this cannot reach — [`SidPrerequisiteKind::None`] and
/// [`SidPrerequisiteKind::Refused`] — are positive claims about an engine, and
/// a dialect makes them by overriding, never by writing a script this reads.
#[must_use]
pub fn kind_of_prerequisite_script(script: &str) -> SidPrerequisiteKind {
    if script.trim().is_empty() {
        SidPrerequisiteKind::Undeclared
    } else if statements_in_prerequisite_script(script).is_empty() {
        SidPrerequisiteKind::OperatorArtifact
    } else {
        SidPrerequisiteKind::SqlInstallable
    }
}

// ════════════════════════════════════════════════════════════════════════
// The external artifact that IS `generate_sid`
// ════════════════════════════════════════════════════════════════════════

/// **The identity of the artifact the bundle's prerequisites step installs as
/// `generate_sid`** — where one exists.
///
/// Returned by [`ReplicationRenderer::sid_external_artifact`], and the only
/// thing a dialect still has to say about the key function's *body*: on every
/// platform where razeri owns the name the body is
/// Razeri's one surrogate-key derivation and reads out of one table, so
/// there is nothing per-dialect left to declare.
///
/// This is an **identity, never a body**: a dialect names the artifact, and the
/// derivation lives inside the artifact. A dialect carries no SQL spelling of
/// the key function's body — a second spelling of the derivation is one that
/// drifts. `generate_sid`'s parameter has **one** name, `input_value`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SidExternalArtifact {
    /// Language name, e.g. `"tsql"`, `"C"`, `"javascript"`, `"python"`.
    pub language: String,
    /// Path or name of the external library, assembly, or emitted definition.
    pub object_file: String,
    /// Fully-qualified symbol within the library.
    pub link_symbol: String,
    /// Descriptive comment for the function — the **dialect's**, because *why
    /// the platform's built-in was refused* is a platform fact.
    pub comment: String,
}

/// **What loads a compiled key-function library**, which decides what the
/// operator does with it once it is built.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SidLibraryLoader {
    /// The client loads it into each connection it opens, as `SQLite`'s
    /// `.load` does. Nothing is installed on a server.
    Client,
    /// The server loads it from its own plugin directory, and one statement
    /// registers it for every session, as `CREATE FUNCTION … SONAME` does on
    /// `MySQL` and `MariaDB`.
    Server,
}

/// **A library's source, shipped in the deploy bundle for the operator to
/// compile** — returned by [`ReplicationRenderer::sid_library_source`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SidLibrarySource {
    /// The name the bundle writes the source under, e.g. `razeri_sid.c`.
    pub file_name: String,
    /// The source itself.
    pub source: String,
    /// The compiled library's name without its suffix, e.g. `razeri_sid`. The
    /// build writes it with the suffix the operating system gives a shared
    /// library (`.so`, `.dylib`, `.dll`).
    pub library: String,
    /// The function called when the library is loaded.
    pub entry_point: String,
    /// The one line that builds it, for the platform most operators have.
    pub build: String,
    /// What loads it: the client, into each connection, or the server, once.
    pub loaded_by: SidLibraryLoader,
}

// ════════════════════════════════════════════════════════════════════════
// ReplicationRenderer Trait
// ════════════════════════════════════════════════════════════════════════

/// Rendering methods for replication operations: SID generation, data
/// extraction, data loading, and checksum validation.
///
/// Defaults produce ANSI-safe SQL so extension dialects (e.g., `SQLite`) work
/// without implementing every method.
pub trait ReplicationRenderer: SqlRenderer {
    // ── The session, before anything runs in it ──────────────────────

    /// **Statements every generated script runs before its first real
    /// statement**, in this order, in the same session.
    ///
    /// Some engines decide how a literal is read from *session settings* rather
    /// than from the statement. Where that is true, a cast that is ordinary
    /// everywhere else fails on the first row of the first load until the
    /// session has been told how to read it — a date literal parsed by a
    /// session-wide format is the measured case. There is nowhere else to put
    /// such a statement: it belongs to the connection, not to any object.
    ///
    /// # What belongs here
    ///
    /// Settings that change **how the engine reads what follows** and are lost
    /// when the session ends: how literals parse, how identifiers fold, which
    /// time zone a timestamp is stamped in, how strings compare.
    ///
    /// # What does not
    ///
    /// Anything that **creates something**. A function, an extension, an
    /// assembly, a table — none of those is a session setting, and putting one
    /// here means running it again on every connection. Those are
    /// [`Self::sid_prerequisites`] where they install the key function, and
    /// ordinary DDL otherwise.
    ///
    /// Each string is one statement without its terminator; the script that
    /// emits them adds whatever this engine ends a statement with. Every
    /// statement must be safe to run again — a script that runs twice runs
    /// these twice.
    ///
    /// The default is empty, and an engine that reads nothing off the session
    /// leaves it that way: nothing is emitted, and its scripts are byte for
    /// byte what they were.
    fn session_prerequisites(&self) -> Vec<String> {
        Vec::new()
    }

    // ── SID (Surrogate ID) Generation ────────────────────────────────

    /// **Does an external artifact already carry the name `generate_sid` on
    /// this platform, and what is it?**
    ///
    /// `None` — the default, and the honest answer for most engines — means
    /// razeri itself defines the key function here: the body is Razeri's one
    /// surrogate-key derivation with the platform's own hash inlined, composed
    /// from the platform name alone, so there is nothing for a dialect to
    /// spell.
    ///
    /// `Some(…)` names what the bundle's prerequisites step installs: SQL
    /// Server's CLR assembly, MySQL/MariaDB's C UDF, Snowflake's emitted
    /// JavaScript UDF, Databricks' emitted Python UDF. The derivation is inside
    /// the artifact rather than in any SQL razeri emits, so what a dialect
    /// states is the artifact's *identity*, matched against the very
    /// prerequisites text that installs it. **This is an identity, not a
    /// body** — see [`SidExternalArtifact`].
    ///
    /// A platform that can be given no function at all answers `None` here too
    /// and says so through [`Self::sid_prerequisite_kind`]
    /// ([`SidPrerequisiteKind::Refused`]). Razeri's conformance checks require
    /// that refusal to be said out loud: where no artifact is declared and
    /// razeri emits no helper either, a dialect that has simply declared
    /// nothing is reported, which is the distinction
    /// [`SidPrerequisiteKind::Undeclared`] exists to keep.
    fn sid_external_artifact(&self) -> Option<SidExternalArtifact> {
        None
    }

    /// **The source of a library the operator compiles**, where the key
    /// function arrives that way — shipped in the deploy bundle as a file of
    /// its own, so the build is one line.
    ///
    /// `SQLite` answers it: its SQL has no `CREATE FUNCTION`, and its client
    /// loads a compiled extension into each connection it opens. `MySQL` and
    /// `MariaDB` answer it too: their server loads a compiled function from
    /// its plugin directory. The default is `None`: nothing ships as a
    /// separate source file.
    fn sid_library_source(&self) -> Option<SidLibrarySource> {
        None
    }

    /// Returns prerequisite SQL/instructions for SID deployment.
    ///
    /// For platforms that require extensions or external libraries (`PostgreSQL`
    /// `pg_xxhash`, SQL Server CLR, `MariaDB` C UDF), this returns the deployment
    /// script. Default: empty (no prerequisites needed).
    fn sid_prerequisites(&self) -> String {
        String::new()
    }

    /// **The executable statements inside [`Self::sid_prerequisites`], in
    /// order** — razeri's own text, split on razeri's own declared boundary.
    ///
    /// This exists so a deploy path or a test harness can *install what razeri
    /// emits* rather than a transcription of it — a hand-written copy is a
    /// second implementation that drifts, and a second splitter would be a
    /// second definition of the boundary.
    ///
    /// **A dialect that does not write
    /// [`SID_PREREQUISITE_STATEMENT_DELIMITER`] declares no executable
    /// statements**, and gets an empty vector. That is the honest answer for
    /// every arm whose prerequisite is an operator's compile step (SQL Server's
    /// CLR assembly, `MariaDB`'s and `MySQL`'s `.so`) and for the platforms with
    /// no prerequisite at all — it is *not* a promise that a caller can run the
    /// prose.
    ///
    /// Leading comment and blank lines are dropped from each segment, so the
    /// prose that explains a statement travels with it in the script and does
    /// not travel to the engine. Comments *inside* a statement survive: they are
    /// part of the statement the operator would paste.
    ///
    /// **The trailing terminator is dropped here, once.** A `;` is how the
    /// script reads to a human pasting the whole file, and is not part of a
    /// single statement over a REST statement API — Snowflake's and Databricks'
    /// both take exactly one. Stripping it in each consumer instead would put
    /// the same rule in the cloud arms *and* in the bundle executor, which
    /// is the shape this method exists to prevent. `sid_prerequisites()`' text
    /// is unchanged and keeps its semicolons.
    fn sid_prerequisite_statements(&self) -> Vec<String> {
        statements_in_prerequisite_script(&self.sid_prerequisites())
    }

    /// **A prerequisite script that installs the same key function without
    /// compiling anything** — where the platform has one, and `None` where it
    /// does not.
    ///
    /// The default is `None`: most platforms have exactly one prerequisite and
    /// [`Self::sid_prerequisites`] is it. A dialect answers `Some` only where
    /// **two honest routes to the same number** exist and the choice belongs to
    /// whoever deploys rather than to whoever designed — the case being a
    /// managed `PostgreSQL` service (`RDS`, Aurora, Cloud SQL), where nobody has
    /// a shell on the server host and so the compiled extension the ordinary
    /// step names can never be built.
    ///
    /// # What answering `Some` promises
    ///
    /// **The same number, byte for byte.** Not a near-enough hash and not a
    /// different algorithm: the surrogate-key rule is one hash and one
    /// derivation everywhere, so a key minted through this route and a key
    /// minted through the compiled one are the same `BIGINT` and a warehouse can
    /// be moved between them without re-keying a row. A dialect that cannot
    /// promise that answers `None` and lets the ordinary step refuse.
    ///
    /// It is a **deploy-time** choice and nothing about it reaches a design: the
    /// text swaps into `prerequisites.sql` alone, `schema.sql` is unchanged, and
    /// every call site still calls the one public name.
    fn sid_pure_sql_fallback(&self) -> Option<String> {
        None
    }

    /// **[`Self::sid_pure_sql_fallback`], installed in every schema a
    /// design writes into.**
    ///
    /// `schemas` names them, in the order the design first writes to each,
    /// spelled as the rendered SQL spells them. A deploy bundle asks this
    /// rather than the plain method, because it knows the schemas and the
    /// plain method does not.
    ///
    /// The default ignores the list and answers the plain method. That is
    /// right wherever an installed function is visible from every schema: a
    /// function on the session's search path, or one with a schema of its own
    /// that every call names.
    ///
    /// Override it where a routine resolves a bare function name in the
    /// schema that owns the routine, and a key function created by plain SQL
    /// has to live in that same schema. There one definition is not enough:
    /// the answer installs the function once in each schema on the list, and
    /// makes each schema first if it is not there yet, because this step runs
    /// before `schema.sql` does. An empty list means the session's current
    /// schema, which is what the plain method answers.
    fn sid_pure_sql_fallback_in(&self, schemas: &[&str]) -> Option<String> {
        let _ = schemas;
        self.sid_pure_sql_fallback()
    }

    /// **Which of [`SidPrerequisiteKind`]'s shapes this platform's prerequisite
    /// is.**
    ///
    /// Derived from what the dialect already says, so the classification cannot
    /// drift from the text it classifies:
    ///
    /// | [`sid_prerequisites`](Self::sid_prerequisites) | [`sid_prerequisite_statements`](Self::sid_prerequisite_statements) | kind |
    /// |---|---|---|
    /// | empty | — | [`Undeclared`](SidPrerequisiteKind::Undeclared) |
    /// | prose **and** statements | non-empty | [`SqlInstallable`](SidPrerequisiteKind::SqlInstallable) |
    /// | prose only | empty | [`OperatorArtifact`](SidPrerequisiteKind::OperatorArtifact) |
    ///
    /// # Silence is `Undeclared`, never `None`
    ///
    /// The two answers the derivation **cannot** reach are the two that are
    /// *positive claims about an engine*:
    ///
    /// - [`None`](SidPrerequisiteKind::None) says the platform's own builtin
    ///   returns the canonical number. That is a measurement, not an absence, and
    ///   a dialect claiming it overrides this method and cites the arm that
    ///   measured it;
    /// - [`Refused`](SidPrerequisiteKind::Refused) says no SQL can ever install
    ///   the function here, which a platform with a native hash and a lazy
    ///   dialect author would also spell as silence.
    ///
    /// It is the same asymmetry `DdlRenderer::representable_timestamp_ceiling`
    /// documents on its own `None`: *not measured* and *nothing to worry about*
    /// are opposite facts that read identically when a default supplies them.
    fn sid_prerequisite_kind(&self) -> SidPrerequisiteKind {
        kind_of_prerequisite_script(&self.sid_prerequisites())
    }

    /// **Where [`Self::sid_prerequisites`] puts what it installs**, so that a
    /// key function created in some other schema can call it by name.
    ///
    /// The default is [`SidPrerequisiteHome::Ambient`]: whatever the
    /// prerequisite creates, it creates wherever the deploy runs, and every
    /// call site names it bare. That is the right answer on every engine whose
    /// prerequisite installs nothing with a schema — a session load, a
    /// server-wide library, a function whose name has no qualifier — and it
    /// leaves those dialects' scripts exactly as they were.
    ///
    /// Answer [`SidPrerequisiteHome::Schema`] where a routine on this engine
    /// resolves an unqualified name in the schema that OWNS it. There, the one
    /// hash a whole warehouse shares needs a fixed address, because the key
    /// function is created once per domain and only one of those domains can be
    /// the one the deploy signed in as.
    ///
    /// See [`SidPrerequisiteHome::Schema`] for what answering it obliges the
    /// dialect's own prerequisite text to do.
    fn sid_prerequisite_home(&self) -> SidPrerequisiteHome {
        SidPrerequisiteHome::Ambient
    }

    /// Returns a validation query to verify the SID function is operational.
    ///
    /// Default: schema-qualified `SELECT generate_sid('test')`.
    fn sid_validation_query(&self, schema: Option<&str>, null_sid: i64) -> String {
        let qualified = match schema {
            Some(s) => format!("{s}.generate_sid"),
            None => "generate_sid".to_string(),
        };
        format!("SELECT {qualified}('test') AS test_sid, {null_sid} AS null_sid;\n")
    }

    // ── Data Extraction ──────────────────────────────────────────────

    /// Renders an extract/export SQL statement for the given table.
    ///
    /// The `format` parameter is one of: `"csv"`, `"parquet"`, `"json_lines"`, `"native"`.
    ///
    /// Default: ANSI `SELECT ... FROM ... WHERE ...` (no COPY).
    fn render_data_extract(
        &self,
        qualified: &str,
        columns: &str,
        file_path: &str,
        format: &str,
        where_clause: &str,
        _batch_size: Option<usize>,
    ) -> String {
        let _ = file_path;
        let _ = format;
        if where_clause.is_empty() {
            format!("SELECT {columns} FROM {qualified};\n")
        } else {
            format!("SELECT {columns} FROM {qualified} {where_clause};\n")
        }
    }

    // ── Data Loading ─────────────────────────────────────────────────

    /// Renders a load/import SQL statement for the given table.
    ///
    /// The `format` parameter is one of: `"csv"`, `"parquet"`, `"json_lines"`, `"native"`.
    ///
    /// Default: ANSI comment placeholder.
    fn render_data_load(&self, qualified: &str, file_path: &str, _format: &str) -> String {
        format!("-- COPY INTO {qualified} FROM '{file_path}';\n")
    }

    // ── Checksum / Validation ────────────────────────────────────────

    /// Renders a checksum validation query for data comparison.
    ///
    /// Default: `SELECT COUNT(*) AS cnt FROM <table>` (no checksum).
    fn render_checksum_query(&self, qualified: &str) -> String {
        format!("SELECT COUNT(*) AS cnt FROM {qualified};\n")
    }

    // ── Platform Warnings ────────────────────────────────────────────

    /// Warning about Parquet export limitations on this platform.
    ///
    /// Default: `None` (no warning needed).
    fn parquet_export_warning(&self) -> Option<&str> {
        None
    }

    /// Warning about SID function deployment requirements on this platform.
    ///
    /// Default: `None` (no special deployment needed).
    fn sid_deployment_warning(&self) -> Option<&str> {
        None
    }

    /// General replication caveats for this platform as a target.
    ///
    /// Default: empty (no platform-specific caveats).
    fn replication_target_warnings(&self) -> Vec<String> {
        Vec::new()
    }

    /// Warning about TRUNCATE behavior on this platform.
    ///
    /// Default: `None` (TRUNCATE works normally).
    fn truncate_caveat(&self) -> Option<&str> {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::{ReplicationRenderer, SID_PREREQUISITE_STATEMENT_DELIMITER};
    use crate::agnostic::AnsiDialect;

    /// A dialect whose prerequisites are whatever a test hands it.
    struct Emitting(String);

    impl crate::SqlRenderer for Emitting {
        fn dialect_name(&self) -> &'static str {
            "emitting"
        }
        fn render_data_type(&self, dt: &crate::DataType) -> String {
            AnsiDialect.render_data_type(dt)
        }
        fn quote_identifier(&self, name: &str) -> String {
            AnsiDialect.quote_identifier(name)
        }
        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
            AnsiDialect.qualify_name(schema, name)
        }
        fn render_current_timestamp(&self) -> String {
            AnsiDialect.render_current_timestamp()
        }
    }
    impl crate::DdlRenderer for Emitting {}
    impl crate::FormulaRenderer for Emitting {}
    impl ReplicationRenderer for Emitting {
        fn sid_prerequisites(&self) -> String {
            self.0.clone()
        }
    }

    /// **A JavaScript body is nothing but semicolons**, which is exactly why the
    /// boundary is declared instead of inferred. Splitting the text below on
    /// `;` yields five fragments, none of them a statement.
    #[test]
    fn the_delimiter_recovers_statements_a_semicolon_split_would_fracture() {
        let script = format!(
            "-- prose the operator reads\n\
             {SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
             -- why this one exists\n\
             CREATE FUNCTION f() AS $$\n  var a = 1; var b = 2; return a + b;\n$$;\n\
             {SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
             CREATE FUNCTION g() AS $$ 1 $$;\n"
        );
        let statements = Emitting(script).sid_prerequisite_statements();
        assert_eq!(statements.len(), 2, "{statements:#?}");
        assert!(statements[0].starts_with("CREATE FUNCTION f()"), "{:?}", statements[0]);
        assert!(statements[0].contains("var a = 1; var b = 2; return a + b;"));
        assert_eq!(statements[1], "CREATE FUNCTION g() AS $$ 1 $$");
    }

    /// **The trailing terminator is dropped exactly once, and only at the end.**
    ///
    /// A REST statement API takes one statement and no terminator; a human
    /// pasting the script wants the `;`. Both are served by leaving it in
    /// `sid_prerequisites()`' text and removing it here — once, so a body whose
    /// own last character is a `;` (every JavaScript function) keeps it.
    #[test]
    fn one_trailing_terminator_is_dropped_and_the_bodys_own_semicolons_survive() {
        let script = format!(
            "{SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
             CREATE FUNCTION f() AS $$\n  return 1;\n$$;\n\
             {SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
             CREATE FUNCTION g() AS $$ 1 $$\n"
        );
        let statements = Emitting(script).sid_prerequisite_statements();
        assert_eq!(statements[0], "CREATE FUNCTION f() AS $$\n  return 1;\n$$");
        assert!(statements[0].contains("return 1;"), "the body's own `;` is not a terminator");
        assert_eq!(
            statements[1], "CREATE FUNCTION g() AS $$ 1 $$",
            "a statement with no terminator is unchanged"
        );
    }

    /// **No delimiter means no executable statements, and that is an answer.**
    /// The prerequisite scripts for SQL Server, `MySQL` and `MariaDB` are an
    /// operator's compile recipe wrapped in comments; handing a caller the prose
    /// as if it were DDL is how a deferral turns into a failure nobody can read.
    #[test]
    fn an_undelimited_script_declares_no_executable_statements() {
        let operator_recipe = "-- compile this yourself:\n/* int main() { } */\n".to_owned();
        assert!(Emitting(operator_recipe).sid_prerequisite_statements().is_empty());
        assert!(AnsiDialect.sid_prerequisite_statements().is_empty());
    }
}