Skip to main content

razeri_plugin_contract/
replication.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! Replication rendering traits with ANSI defaults.
6//!
7//! [`ReplicationRenderer`] extends [`SqlRenderer`] with methods for
8//! SID (surrogate ID) function generation, data extraction, data loading, and
9//! checksum validation. Each dialect crate overrides these methods to produce
10//! platform-native SQL.
11//!
12//! ## Format names
13//!
14//! Methods that accept a `format` parameter use well-known string identifiers:
15//!
16//! | Name | Description |
17//! |------|-------------|
18//! | `"csv"` | Comma-separated values |
19//! | `"parquet"` | Apache Parquet columnar format |
20//! | `"json_lines"` | JSON Lines (one object per line) |
21//! | `"native"` | Platform-native binary format |
22
23use crate::SqlRenderer;
24
25// ════════════════════════════════════════════════════════════════════════
26// SID Prerequisites — the statement boundary
27// ════════════════════════════════════════════════════════════════════════
28
29/// **The line a dialect writes between two independently-executable statements
30/// inside [`ReplicationRenderer::sid_prerequisites`].**
31///
32/// A prerequisite script can be mostly prose — the `MariaDB` C source is
33/// emitted as a comment block for an operator to compile — but several arms
34/// emit real DDL a machine must run one statement at a time: Snowflake's
35/// JavaScript UDF plus its SQL wrapper, Databricks' Python UDF, SQL Server's
36/// assembly installed from its bytes. The first two cannot be recovered by
37/// splitting on `;`, because **both bodies contain semicolons**: a JavaScript
38/// function body is nothing but semicolons.
39///
40/// So the boundary is declared rather than inferred. The delimiter is a SQL
41/// comment, so a script carrying it is still one paste-able file for a human
42/// operator, and [`ReplicationRenderer::sid_prerequisite_statements`] is the one
43/// splitter — a consumer that re-derived the boundary would be a second
44/// definition of it, and a second definition is how the two drift apart. Do not
45/// write your own splitter; split on this constant or call the trait method.
46pub const SID_PREREQUISITE_STATEMENT_DELIMITER: &str =
47    "/* ---- RAZERI SID PREREQUISITE STATEMENT ---- */";
48
49/// **The input every generated prerequisite probe asks about.**
50///
51/// One word, and it is the one [`ReplicationRenderer::sid_validation_query`]
52/// already puts in its `SELECT`. It lives here because three producers must
53/// agree on it and they live in different crates: the dialect writing the
54/// operator's instructions, the generator writing the deploy script's
55/// comparison, and the validation query itself. A probe that asked about one
56/// word while the instructions promised another would be a second, hand-spelled
57/// variant of the same fact — the kind of duplicate that drifts unnoticed.
58pub const SID_PROBE_INPUT: &str = "test";
59
60// ════════════════════════════════════════════════════════════════════════
61// SID Prerequisites — where the helper they install lives
62// ════════════════════════════════════════════════════════════════════════
63
64/// **The schema Razeri keeps its own engine-level objects in**, where an engine
65/// has schemas and the dialect asks for one.
66///
67/// One word, and it is Razeri's rather than the design's. The hash a key
68/// function calls is installed once for the whole engine, and a design has as
69/// many schemas as it has domains: putting the hash in one of them would make
70/// every other domain depend on that domain's schema, and domains are meant to
71/// be deployable apart. So the helper gets a home of its own, named here so
72/// that a dialect, a deploy script and a test all read the same word.
73pub const RAZERI_HELPER_SCHEMA: &str = "razeri";
74
75/// **Where the objects a prerequisite installs actually live**, so that
76/// something calling them from another schema can name them.
77///
78/// Most engines need no answer. Their prerequisite installs nothing with a
79/// name — a session setting, an extension registered for the whole database, a
80/// server-global function — or installs the key function itself, which Razeri
81/// already creates and calls in a known schema. Those say [`Ambient`] and their
82/// scripts are what they were, byte for byte.
83///
84/// [`Schema`] is for an engine where a routine resolves an unqualified name in
85/// the schema that OWNS the routine rather than in the session's. There, a hash
86/// installed wherever the deploy happened to sign in is invisible to every key
87/// function created in a design's own schema — measured on Oracle as
88/// `ORA-00904: "RAZERI_XXH64": invalid identifier`, once, with every function
89/// beneath it then `ORA-06575 … in an invalid state`. Naming the home fixes the
90/// helper in one place and lets every call site spell it.
91///
92/// [`Ambient`]: Self::Ambient
93/// [`Schema`]: Self::Schema
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SidPrerequisiteHome {
96    /// **Wherever the deploy runs** — the default, and the honest answer for an
97    /// engine with no schema for a function at all.
98    ///
99    /// `SQLite` loads a library into each connection; `MySQL` and `MariaDB`
100    /// register a library for the whole server; `ClickHouse`'s `CREATE FUNCTION` takes no
101    /// qualifier; `DuckDB` loads an extension into the session. None of them
102    /// has a schema to put a helper in, and a call site that invented one would
103    /// name an object that does not exist.
104    Ambient,
105
106    /// **A schema Razeri owns**, named here, which the prerequisite creates and
107    /// installs into and which every call site then qualifies with.
108    ///
109    /// Use [`RAZERI_HELPER_SCHEMA`] unless your engine reserves that word: one
110    /// home for one engine-level helper is the point, and two dialects naming
111    /// two homes would be two places to look.
112    ///
113    /// **What answering this promises.** The prerequisite's own text creates
114    /// its objects IN that schema, and makes them callable from outside it —
115    /// on an engine with privileges, that means the grant travels with the
116    /// install. Razeri writes the schema's own creation in front of the
117    /// install, in whatever this engine's `CREATE SCHEMA` spelling is, and
118    /// qualifies the hash in every key function it composes.
119    Schema(&'static str),
120}
121
122impl SidPrerequisiteHome {
123    /// The schema this home names, or `None` where there is none to name.
124    #[must_use]
125    pub const fn schema(self) -> Option<&'static str> {
126        match self {
127            Self::Ambient => None,
128            Self::Schema(schema) => Some(schema),
129        }
130    }
131}
132
133// ════════════════════════════════════════════════════════════════════════
134// SID Prerequisites — what KIND of step this platform's is
135// ════════════════════════════════════════════════════════════════════════
136
137/// **What a platform's SID prerequisite actually asks of whoever deploys.**
138///
139/// The bundle's prerequisites step has to emit a different *shape* for each of
140/// these, and the deploy script a different *behaviour*: one runs SQL, one
141/// verifies and refuses, one refuses outright, one says there is nothing to do.
142/// Sniffing that out of the prose at each call site would put the rule in the
143/// generator, the script writer and the runner — three readings of one fact,
144/// which is exactly how one derivation becomes two.
145///
146/// The default derives it from what the dialect already says, so a dialect that
147/// overrides nothing is still classified correctly and only a platform the
148/// derivation cannot reach ([`Refused`](Self::Refused), [`None`](Self::None))
149/// has to override.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum SidPrerequisiteKind {
152    /// **Nothing to install: the engine's own hash already IS the canonical
153    /// number** (XXH64 seed 0), reachable from SQL razeri emits.
154    ///
155    /// **A positive claim, and therefore never a default.** Saying this means
156    /// razeri has *measured* that the platform's builtin returns XXH64 seed 0 —
157    /// not that nobody has said otherwise. A dialect that has declared nothing
158    /// gets [`Undeclared`](Self::Undeclared) instead, for the same reason
159    /// `DdlRenderer::representable_timestamp_ceiling`'s `None` means *no
160    /// measured ceiling* rather than *holds everything*.
161    ///
162    /// The derivation cannot reach this variant. A dialect claiming it
163    /// **overrides [`ReplicationRenderer::sid_prerequisite_kind`] and cites the
164    /// measurement**, as Razeri's own `ClickHouse` dialect does.
165    None,
166
167    /// Razeri emits statements that install it, and a deploy script can run
168    /// them unattended: `INSTALL`/`LOAD` (`DuckDB`), `CREATE FUNCTION`
169    /// (Snowflake's JavaScript UDF, Databricks' Python one).
170    ///
171    /// The test is *"can a script finish this with no human on the host"*. It is
172    /// **not** "is the statement SQL": `CREATE EXTENSION` is SQL and only
173    /// **registers** a shared object somebody compiled first, which is why
174    /// `PostgreSQL` and Citus are [`OperatorArtifact`](Self::OperatorArtifact)
175    /// and not this.
176    SqlInstallable,
177
178    /// An artifact the **operator** builds and places before any SQL can
179    /// reference it — `MariaDB`'s and `MySQL`'s `.so`, `PostgreSQL`'s and
180    /// Citus's `pg_xxhash`. Razeri emits the source
181    /// or the build recipe as prose; it cannot compile anything, and it will not
182    /// pretend to.
183    ///
184    /// The deploy path's obligation here is the prerequisites verb: **verify,
185    /// then
186    /// refuse** — ask the engine for the function and stop in front of `schema`
187    /// when it is not there, rather than deploying a warehouse whose keys it
188    /// cannot compute.
189    OperatorArtifact,
190
191    /// The platform cannot be made to compute razeri's key from SQL **at all**,
192    /// and no artifact an operator drops in changes that.
193    ///
194    /// Redshift is the case: its `prerequisites.sql` says why. A platform with
195    /// no `CREATE FUNCTION` whose client can load a compiled library is not
196    /// this case — `SQLite` ships its key function as a library the operator
197    /// compiles, an [`OperatorArtifact`](Self::OperatorArtifact).
198    ///
199    /// The answer is a **named refusal carried in the bundle**, checked before
200    /// any data moves. Never a silent fall-back expression: a number that looks
201    /// like a key and is not razeri's writes keys no other deployment can
202    /// reproduce.
203    Refused,
204
205    /// **Nothing to install here, because razeri's own key function travels in
206    /// `schema.sql` instead** — one definition in every namespace whose
207    /// objects call it, created beside those objects, so a namespace deploys
208    /// on its own.
209    ///
210    /// `BigQuery` is the case and the reason is name resolution, not taste: a
211    /// bare `generate_sid('test')` is *"Function not found: `generate_sid`; Did
212    /// you mean `generate_uuid`?"* there — no search path, no session schema, no
213    /// default dataset inside a procedure body — so every call site must name
214    /// the dataset the function was created in, and a prerequisites script has
215    /// no dataset to create it in. The definition therefore rides `schema.sql`,
216    /// where the domain's namespace is known.
217    ///
218    /// It is **not** [`None`](Self::None). That variant is the positive claim
219    /// *"the engine's own hash IS the canonical number"*, which is false here:
220    /// `BigQuery` has no XXH64 in any spelling and the key is a razeri-owned
221    /// JavaScript UDF. What the two share is only the deploy behaviour —
222    /// nothing to run before `schema.sql` — and sharing a behaviour is not
223    /// sharing a claim.
224    EmittedInSchema,
225
226    /// **Razeri has no measured answer for this platform** — the honest default,
227    /// and the one a dialect gets by saying nothing.
228    ///
229    /// # Why this variant exists
230    ///
231    /// A derivation that answered [`None`](Self::None) here would turn *"this
232    /// dialect declared nothing"* into the positive claim *"its own hash is
233    /// razeri's number"* — a claim that has been false on real engines. Silence
234    /// and safety must never read the same, so silence gets its own variant.
235    ///
236    /// # What it means to a deploy path
237    ///
238    /// The same as [`OperatorArtifact`](Self::OperatorArtifact): **verify, then
239    /// refuse.** Safe-by-default is the only defensible reading of "razeri does
240    /// not know" — the contract allows an emitted UDF or a named refusal and
241    /// explicitly not a local approximation, and proceeding on an unmeasured
242    /// platform is neither.
243    ///
244    /// A dialect that genuinely needs no prerequisite says so by overriding
245    /// [`ReplicationRenderer::sid_prerequisite_kind`] to
246    /// [`None`](Self::None), with its measurement cited.
247    Undeclared,
248}
249
250/// **The executable statements inside a prerequisite script**, split on
251/// [`SID_PREREQUISITE_STATEMENT_DELIMITER`].
252///
253/// The body of [`ReplicationRenderer::sid_prerequisite_statements`], lifted out
254/// so a caller holding a script that is not `self.sid_prerequisites()` — the
255/// bundle, when it swaps in
256/// [`ReplicationRenderer::sid_pure_sql_fallback`] — splits it the same way
257/// rather than growing a second reading of the boundary.
258///
259/// Leading comment and blank lines are dropped from each segment and one
260/// trailing terminator is stripped; see the trait method for why both.
261#[must_use]
262pub fn statements_in_prerequisite_script(script: &str) -> Vec<String> {
263    if !script.contains(SID_PREREQUISITE_STATEMENT_DELIMITER) {
264        return Vec::new();
265    }
266    script
267        .split(SID_PREREQUISITE_STATEMENT_DELIMITER)
268        .filter_map(|segment| {
269            // The comment lines in front of the statement, block or line, and
270            // the blank lines among them, are the operator's, not the engine's.
271            let lines: Vec<&str> = segment.lines().collect();
272            let leading = lines
273                .iter()
274                .zip(crate::comment::comment_line_flags(segment))
275                .take_while(|(line, is_note)| *is_note || line.trim().is_empty())
276                .count();
277            let statement = lines[leading..].join("\n");
278            // One trailing terminator, not a run of them: `$$;` ends a body
279            // whose own last character may be a `;` the statement needs.
280            let statement = statement.trim_end().strip_suffix(';').map_or_else(
281                || statement.trim_end().to_owned(),
282                |trimmed| trimmed.trim_end().to_owned(),
283            );
284            (!statement.is_empty()).then_some(statement)
285        })
286        .collect()
287}
288
289/// **What shape a prerequisite script is**, read off the script itself.
290///
291/// The body of [`ReplicationRenderer::sid_prerequisite_kind`]'s default, lifted
292/// out for the same reason as [`statements_in_prerequisite_script`]: a caller
293/// classifying a script the dialect did not return from `sid_prerequisites()`
294/// must reach the same three answers by the same rule. A second reading of the
295/// table in the trait method's own doc comment is how a script comes to be
296/// classified two ways.
297///
298/// The two answers this cannot reach — [`SidPrerequisiteKind::None`] and
299/// [`SidPrerequisiteKind::Refused`] — are positive claims about an engine, and
300/// a dialect makes them by overriding, never by writing a script this reads.
301#[must_use]
302pub fn kind_of_prerequisite_script(script: &str) -> SidPrerequisiteKind {
303    if script.trim().is_empty() {
304        SidPrerequisiteKind::Undeclared
305    } else if statements_in_prerequisite_script(script).is_empty() {
306        SidPrerequisiteKind::OperatorArtifact
307    } else {
308        SidPrerequisiteKind::SqlInstallable
309    }
310}
311
312// ════════════════════════════════════════════════════════════════════════
313// The external artifact that IS `generate_sid`
314// ════════════════════════════════════════════════════════════════════════
315
316/// **The identity of the artifact the bundle's prerequisites step installs as
317/// `generate_sid`** — where one exists.
318///
319/// Returned by [`ReplicationRenderer::sid_external_artifact`], and the only
320/// thing a dialect still has to say about the key function's *body*: on every
321/// platform where razeri owns the name the body is
322/// Razeri's one surrogate-key derivation and reads out of one table, so
323/// there is nothing per-dialect left to declare.
324///
325/// This is an **identity, never a body**: a dialect names the artifact, and the
326/// derivation lives inside the artifact. A dialect carries no SQL spelling of
327/// the key function's body — a second spelling of the derivation is one that
328/// drifts. `generate_sid`'s parameter has **one** name, `input_value`.
329#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct SidExternalArtifact {
331    /// Language name, e.g. `"tsql"`, `"C"`, `"javascript"`, `"python"`.
332    pub language: String,
333    /// Path or name of the external library, assembly, or emitted definition.
334    pub object_file: String,
335    /// Fully-qualified symbol within the library.
336    pub link_symbol: String,
337    /// Descriptive comment for the function — the **dialect's**, because *why
338    /// the platform's built-in was refused* is a platform fact.
339    pub comment: String,
340}
341
342/// **What loads a compiled key-function library**, which decides what the
343/// operator does with it once it is built.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum SidLibraryLoader {
346    /// The client loads it into each connection it opens, as `SQLite`'s
347    /// `.load` does. Nothing is installed on a server.
348    Client,
349    /// The server loads it from its own plugin directory, and one statement
350    /// registers it for every session, as `CREATE FUNCTION … SONAME` does on
351    /// `MySQL` and `MariaDB`.
352    Server,
353}
354
355/// **A library's source, shipped in the deploy bundle for the operator to
356/// compile** — returned by [`ReplicationRenderer::sid_library_source`].
357#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct SidLibrarySource {
359    /// The name the bundle writes the source under, e.g. `razeri_sid.c`.
360    pub file_name: String,
361    /// The source itself.
362    pub source: String,
363    /// The compiled library's name without its suffix, e.g. `razeri_sid`. The
364    /// build writes it with the suffix the operating system gives a shared
365    /// library (`.so`, `.dylib`, `.dll`).
366    pub library: String,
367    /// The function called when the library is loaded.
368    pub entry_point: String,
369    /// The one line that builds it, for the platform most operators have.
370    pub build: String,
371    /// What loads it: the client, into each connection, or the server, once.
372    pub loaded_by: SidLibraryLoader,
373}
374
375// ════════════════════════════════════════════════════════════════════════
376// ReplicationRenderer Trait
377// ════════════════════════════════════════════════════════════════════════
378
379/// Rendering methods for replication operations: SID generation, data
380/// extraction, data loading, and checksum validation.
381///
382/// Defaults produce ANSI-safe SQL so extension dialects (e.g., `SQLite`) work
383/// without implementing every method.
384pub trait ReplicationRenderer: SqlRenderer {
385    // -- The session, before anything runs in it ----------------------
386
387    /// **Statements every generated script runs before its first real
388    /// statement**, in this order, in the same session.
389    ///
390    /// Some engines decide how a literal is read from *session settings* rather
391    /// than from the statement. Where that is true, a cast that is ordinary
392    /// everywhere else fails on the first row of the first load until the
393    /// session has been told how to read it — a date literal parsed by a
394    /// session-wide format is the measured case. There is nowhere else to put
395    /// such a statement: it belongs to the connection, not to any object.
396    ///
397    /// # What belongs here
398    ///
399    /// Settings that change **how the engine reads what follows** and are lost
400    /// when the session ends: how literals parse, how identifiers fold, which
401    /// time zone a timestamp is stamped in, how strings compare.
402    ///
403    /// # What does not
404    ///
405    /// Anything that **creates something**. A function, an extension, an
406    /// assembly, a table — none of those is a session setting, and putting one
407    /// here means running it again on every connection. Those are
408    /// [`Self::sid_prerequisites`] where they install the key function, and
409    /// ordinary DDL otherwise.
410    ///
411    /// Each string is one statement without its terminator; the script that
412    /// emits them adds whatever this engine ends a statement with. Every
413    /// statement must be safe to run again — a script that runs twice runs
414    /// these twice.
415    ///
416    /// The default is empty, and an engine that reads nothing off the session
417    /// leaves it that way: nothing is emitted, and its scripts are byte for
418    /// byte what they were.
419    fn session_prerequisites(&self) -> Vec<String> {
420        Vec::new()
421    }
422
423    /// **The session's settings for one design**, which may need fewer than
424    /// [`Self::session_prerequisites`] names.
425    ///
426    /// A setting that only a key needs — loading the extension that computes
427    /// it — is wasted on a design that computes no key, and on a machine that
428    /// never installed the extension it is a stop. `computes_key` says whether
429    /// the design's schema calls the key function; the prerequisites step
430    /// installs the key's function under the same condition, so the two agree.
431    ///
432    /// The default hands back [`Self::session_prerequisites`] whole, which is
433    /// right for an engine whose settings never depend on the key.
434    fn session_prerequisites_for(&self, computes_key: bool) -> Vec<String> {
435        let _ = computes_key;
436        self.session_prerequisites()
437    }
438
439    // -- SID (Surrogate ID) Generation --------------------------------
440
441    /// **Does an external artifact already carry the name `generate_sid` on
442    /// this platform, and what is it?**
443    ///
444    /// `None` — the default, and the honest answer for most engines — means
445    /// razeri itself defines the key function here: the body is Razeri's one
446    /// surrogate-key derivation with the platform's own hash inlined, composed
447    /// from the platform name alone, so there is nothing for a dialect to
448    /// spell.
449    ///
450    /// `Some(…)` names what the bundle's prerequisites step installs: SQL
451    /// Server's CLR assembly, MySQL/MariaDB's C UDF, Snowflake's emitted
452    /// JavaScript UDF, Databricks' emitted Python UDF. The derivation is inside
453    /// the artifact rather than in any SQL razeri emits, so what a dialect
454    /// states is the artifact's *identity*, matched against the very
455    /// prerequisites text that installs it. **This is an identity, not a
456    /// body** — see [`SidExternalArtifact`].
457    ///
458    /// A platform that can be given no function at all answers `None` here too
459    /// and says so through [`Self::sid_prerequisite_kind`]
460    /// ([`SidPrerequisiteKind::Refused`]). Razeri's conformance checks require
461    /// that refusal to be said out loud: where no artifact is declared and
462    /// razeri emits no helper either, a dialect that has simply declared
463    /// nothing is reported, which is the distinction
464    /// [`SidPrerequisiteKind::Undeclared`] exists to keep.
465    fn sid_external_artifact(&self) -> Option<SidExternalArtifact> {
466        None
467    }
468
469    /// **The source of a library the operator compiles**, where the key
470    /// function arrives that way — shipped in the deploy bundle as a file of
471    /// its own, so the build is one line.
472    ///
473    /// `SQLite` answers it: its SQL has no `CREATE FUNCTION`, and its client
474    /// loads a compiled extension into each connection it opens. `MySQL` and
475    /// `MariaDB` answer it too: their server loads a compiled function from
476    /// its plugin directory. The default is `None`: nothing ships as a
477    /// separate source file.
478    fn sid_library_source(&self) -> Option<SidLibrarySource> {
479        None
480    }
481
482    /// Returns prerequisite SQL/instructions for SID deployment.
483    ///
484    /// For platforms that require extensions or external libraries (`PostgreSQL`
485    /// `pg_xxhash`, SQL Server CLR, `MariaDB` C UDF), this returns the deployment
486    /// script. Default: empty (no prerequisites needed).
487    fn sid_prerequisites(&self) -> String {
488        String::new()
489    }
490
491    /// **The executable statements inside [`Self::sid_prerequisites`], in
492    /// order** — razeri's own text, split on razeri's own declared boundary.
493    ///
494    /// This exists so a deploy path or a test harness can *install what razeri
495    /// emits* rather than a transcription of it — a hand-written copy is a
496    /// second implementation that drifts, and a second splitter would be a
497    /// second definition of the boundary.
498    ///
499    /// **A dialect that does not write
500    /// [`SID_PREREQUISITE_STATEMENT_DELIMITER`] declares no executable
501    /// statements**, and gets an empty vector. That is the honest answer for
502    /// every arm whose prerequisite is an operator's compile step (`MariaDB`'s
503    /// and `MySQL`'s `.so`) and for the platforms with
504    /// no prerequisite at all — it is *not* a promise that a caller can run the
505    /// prose.
506    ///
507    /// Leading comment and blank lines are dropped from each segment, so the
508    /// prose that explains a statement travels with it in the script and does
509    /// not travel to the engine. Comments *inside* a statement survive: they are
510    /// part of the statement the operator would paste.
511    ///
512    /// **The trailing terminator is dropped here, once.** A `;` is how the
513    /// script reads to a human pasting the whole file, and is not part of a
514    /// single statement over a REST statement API — Snowflake's and Databricks'
515    /// both take exactly one. Stripping it in each consumer instead would put
516    /// the same rule in the cloud arms *and* in the bundle executor, which
517    /// is the shape this method exists to prevent. `sid_prerequisites()`' text
518    /// is unchanged and keeps its semicolons.
519    fn sid_prerequisite_statements(&self) -> Vec<String> {
520        statements_in_prerequisite_script(&self.sid_prerequisites())
521    }
522
523    /// **A prerequisite script that installs the same key function without
524    /// compiling anything** — where the platform has one, and `None` where it
525    /// does not.
526    ///
527    /// The default is `None`: most platforms have exactly one prerequisite and
528    /// [`Self::sid_prerequisites`] is it. A dialect answers `Some` only where
529    /// **two honest routes to the same number** exist and the choice belongs to
530    /// whoever deploys rather than to whoever designed — the case being a
531    /// managed `PostgreSQL` service (`RDS`, Aurora, Cloud SQL), where nobody has
532    /// a shell on the server host and so the compiled extension the ordinary
533    /// step names can never be built.
534    ///
535    /// # What answering `Some` promises
536    ///
537    /// **The same number, byte for byte.** Not a near-enough hash and not a
538    /// different algorithm: the surrogate-key rule is one hash and one
539    /// derivation everywhere, so a key minted through this route and a key
540    /// minted through the compiled one are the same `BIGINT` and a warehouse can
541    /// be moved between them without re-keying a row. A dialect that cannot
542    /// promise that answers `None` and lets the ordinary step refuse.
543    ///
544    /// It is a **deploy-time** choice and nothing about it reaches a design: the
545    /// text swaps into `prerequisites.sql` alone, `schema.sql` is unchanged, and
546    /// every call site still calls the one public name.
547    fn sid_pure_sql_fallback(&self) -> Option<String> {
548        None
549    }
550
551    /// **[`Self::sid_pure_sql_fallback`], installed in every schema a
552    /// design writes into.**
553    ///
554    /// `schemas` names them, in the order the design first writes to each,
555    /// spelled as the rendered SQL spells them. A deploy bundle asks this
556    /// rather than the plain method, because it knows the schemas and the
557    /// plain method does not.
558    ///
559    /// The default ignores the list and answers the plain method. That is
560    /// right wherever an installed function is visible from every schema: a
561    /// function on the session's search path, or one with a schema of its own
562    /// that every call names.
563    ///
564    /// Override it where a routine resolves a bare function name in the
565    /// schema that owns the routine, and a key function created by plain SQL
566    /// has to live in that same schema. There one definition is not enough:
567    /// the answer installs the function once in each schema on the list, and
568    /// makes each schema first if it is not there yet, because this step runs
569    /// before `schema.sql` does. An empty list means the session's current
570    /// schema, which is what the plain method answers.
571    fn sid_pure_sql_fallback_in(&self, schemas: &[&str]) -> Option<String> {
572        let _ = schemas;
573        self.sid_pure_sql_fallback()
574    }
575
576    /// **Which of [`SidPrerequisiteKind`]'s shapes this platform's prerequisite
577    /// is.**
578    ///
579    /// Derived from what the dialect already says, so the classification cannot
580    /// drift from the text it classifies:
581    ///
582    /// | [`sid_prerequisites`](Self::sid_prerequisites) | [`sid_prerequisite_statements`](Self::sid_prerequisite_statements) | kind |
583    /// |---|---|---|
584    /// | empty | — | [`Undeclared`](SidPrerequisiteKind::Undeclared) |
585    /// | prose **and** statements | non-empty | [`SqlInstallable`](SidPrerequisiteKind::SqlInstallable) |
586    /// | prose only | empty | [`OperatorArtifact`](SidPrerequisiteKind::OperatorArtifact) |
587    ///
588    /// # Silence is `Undeclared`, never `None`
589    ///
590    /// The two answers the derivation **cannot** reach are the two that are
591    /// *positive claims about an engine*:
592    ///
593    /// - [`None`](SidPrerequisiteKind::None) says the platform's own builtin
594    ///   returns the canonical number. That is a measurement, not an absence, and
595    ///   a dialect claiming it overrides this method and cites the arm that
596    ///   measured it;
597    /// - [`Refused`](SidPrerequisiteKind::Refused) says no SQL can ever install
598    ///   the function here, which a platform with a native hash and a lazy
599    ///   dialect author would also spell as silence.
600    ///
601    /// It is the same asymmetry `DdlRenderer::representable_timestamp_ceiling`
602    /// documents on its own `None`: *not measured* and *nothing to worry about*
603    /// are opposite facts that read identically when a default supplies them.
604    fn sid_prerequisite_kind(&self) -> SidPrerequisiteKind {
605        kind_of_prerequisite_script(&self.sid_prerequisites())
606    }
607
608    /// **Where [`Self::sid_prerequisites`] puts what it installs**, so that a
609    /// key function created in some other schema can call it by name.
610    ///
611    /// The default is [`SidPrerequisiteHome::Ambient`]: whatever the
612    /// prerequisite creates, it creates wherever the deploy runs, and every
613    /// call site names it bare. That is the right answer on every engine whose
614    /// prerequisite installs nothing with a schema — a session load, a
615    /// server-wide library, a function whose name has no qualifier — and it
616    /// leaves those dialects' scripts exactly as they were.
617    ///
618    /// Answer [`SidPrerequisiteHome::Schema`] where a routine on this engine
619    /// resolves an unqualified name in the schema that OWNS it. There, the one
620    /// hash a whole warehouse shares needs a fixed address, because every layer
621    /// schema holds its own copy of the key function, each calls the one hash,
622    /// and at most one of those schemas is the one the deploy signed in as.
623    ///
624    /// See [`SidPrerequisiteHome::Schema`] for what answering it obliges the
625    /// dialect's own prerequisite text to do.
626    fn sid_prerequisite_home(&self) -> SidPrerequisiteHome {
627        SidPrerequisiteHome::Ambient
628    }
629
630    /// Returns a validation query to verify the SID function is operational.
631    ///
632    /// Default: schema-qualified `SELECT generate_sid('test')`.
633    fn sid_validation_query(&self, schema: Option<&str>, null_sid: i64) -> String {
634        let qualified = match schema {
635            Some(s) => format!("{s}.generate_sid"),
636            None => "generate_sid".to_string(),
637        };
638        format!("SELECT {qualified}('test') AS test_sid, {null_sid} AS null_sid;\n")
639    }
640
641    // -- Data Extraction ----------------------------------------------
642
643    /// Renders an extract/export SQL statement for the given table.
644    ///
645    /// The `format` parameter is one of: `"csv"`, `"parquet"`, `"json_lines"`, `"native"`.
646    ///
647    /// Default: ANSI `SELECT ... FROM ... WHERE ...` (no COPY).
648    fn render_data_extract(
649        &self,
650        qualified: &str,
651        columns: &str,
652        file_path: &str,
653        format: &str,
654        where_clause: &str,
655        _batch_size: Option<usize>,
656    ) -> String {
657        let _ = file_path;
658        let _ = format;
659        if where_clause.is_empty() {
660            format!("SELECT {columns} FROM {qualified};\n")
661        } else {
662            format!("SELECT {columns} FROM {qualified} {where_clause};\n")
663        }
664    }
665
666    // -- Data Loading -------------------------------------------------
667
668    /// Renders a load/import SQL statement for the given table.
669    ///
670    /// The `format` parameter is one of: `"csv"`, `"parquet"`, `"json_lines"`, `"native"`.
671    ///
672    /// Default: ANSI comment placeholder.
673    fn render_data_load(&self, qualified: &str, file_path: &str, _format: &str) -> String {
674        format!(
675            "{said_0}\n",
676            said_0 = crate::comment::block_comment(&format!(
677                "COPY INTO {qualified} FROM '{file_path}';"
678            ))
679        )
680    }
681
682    // -- Checksum / Validation ----------------------------------------
683
684    /// Renders a checksum validation query for data comparison.
685    ///
686    /// Default: `SELECT COUNT(*) AS cnt FROM <table>` (no checksum).
687    fn render_checksum_query(&self, qualified: &str) -> String {
688        format!("SELECT COUNT(*) AS cnt FROM {qualified};\n")
689    }
690
691    // -- Platform Warnings --------------------------------------------
692
693    /// Warning about Parquet export limitations on this platform.
694    ///
695    /// Default: `None` (no warning needed).
696    fn parquet_export_warning(&self) -> Option<&str> {
697        None
698    }
699
700    /// Warning about SID function deployment requirements on this platform.
701    ///
702    /// Default: `None` (no special deployment needed).
703    fn sid_deployment_warning(&self) -> Option<&str> {
704        None
705    }
706
707    /// General replication caveats for this platform as a target.
708    ///
709    /// Default: empty (no platform-specific caveats).
710    fn replication_target_warnings(&self) -> Vec<String> {
711        Vec::new()
712    }
713
714    /// Warning about TRUNCATE behavior on this platform.
715    ///
716    /// Default: `None` (TRUNCATE works normally).
717    fn truncate_caveat(&self) -> Option<&str> {
718        None
719    }
720}
721
722#[cfg(test)]
723mod tests {
724    use super::{ReplicationRenderer, SID_PREREQUISITE_STATEMENT_DELIMITER};
725    use crate::agnostic::AnsiDialect;
726
727    /// A dialect whose prerequisites are whatever a test hands it.
728    struct Emitting(String);
729
730    impl crate::SqlRenderer for Emitting {
731        fn dialect_name(&self) -> &'static str {
732            "emitting"
733        }
734        fn render_data_type(&self, dt: &crate::DataType) -> String {
735            AnsiDialect.render_data_type(dt)
736        }
737        fn quote_identifier(&self, name: &str) -> String {
738            AnsiDialect.quote_identifier(name)
739        }
740        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
741            AnsiDialect.qualify_name(schema, name)
742        }
743        fn render_current_timestamp(&self) -> String {
744            AnsiDialect.render_current_timestamp()
745        }
746    }
747    impl crate::DdlRenderer for Emitting {}
748    impl crate::FormulaRenderer for Emitting {}
749    impl ReplicationRenderer for Emitting {
750        fn sid_prerequisites(&self) -> String {
751            self.0.clone()
752        }
753    }
754
755    /// **A JavaScript body is nothing but semicolons**, which is exactly why the
756    /// boundary is declared instead of inferred. Splitting the text below on
757    /// `;` yields five fragments, none of them a statement.
758    #[test]
759    fn the_delimiter_recovers_statements_a_semicolon_split_would_fracture() {
760        let script = format!(
761            "-- prose the operator reads\n\
762             {SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
763             -- why this one exists\n\
764             CREATE FUNCTION f() AS $$\n  var a = 1; var b = 2; return a + b;\n$$;\n\
765             {SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
766             CREATE FUNCTION g() AS $$ 1 $$;\n"
767        );
768        let statements = Emitting(script).sid_prerequisite_statements();
769        assert_eq!(statements.len(), 2, "{statements:#?}");
770        assert!(statements[0].starts_with("CREATE FUNCTION f()"), "{:?}", statements[0]);
771        assert!(statements[0].contains("var a = 1; var b = 2; return a + b;"));
772        assert_eq!(statements[1], "CREATE FUNCTION g() AS $$ 1 $$");
773    }
774
775    /// **The trailing terminator is dropped exactly once, and only at the end.**
776    ///
777    /// A REST statement API takes one statement and no terminator; a human
778    /// pasting the script wants the `;`. Both are served by leaving it in
779    /// `sid_prerequisites()`' text and removing it here — once, so a body whose
780    /// own last character is a `;` (every JavaScript function) keeps it.
781    #[test]
782    fn one_trailing_terminator_is_dropped_and_the_bodys_own_semicolons_survive() {
783        let script = format!(
784            "{SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
785             CREATE FUNCTION f() AS $$\n  return 1;\n$$;\n\
786             {SID_PREREQUISITE_STATEMENT_DELIMITER}\n\
787             CREATE FUNCTION g() AS $$ 1 $$\n"
788        );
789        let statements = Emitting(script).sid_prerequisite_statements();
790        assert_eq!(statements[0], "CREATE FUNCTION f() AS $$\n  return 1;\n$$");
791        assert!(statements[0].contains("return 1;"), "the body's own `;` is not a terminator");
792        assert_eq!(
793            statements[1], "CREATE FUNCTION g() AS $$ 1 $$",
794            "a statement with no terminator is unchanged"
795        );
796    }
797
798    /// **No delimiter means no executable statements, and that is an answer.**
799    /// The prerequisite scripts for `MySQL` and `MariaDB` are an operator's
800    /// compile recipe wrapped in comments; handing a caller the prose
801    /// as if it were DDL is how a deferral turns into a failure nobody can read.
802    #[test]
803    fn an_undelimited_script_declares_no_executable_statements() {
804        let operator_recipe = "-- compile this yourself:\n/* int main() { } */\n".to_owned();
805        assert!(Emitting(operator_recipe).sid_prerequisite_statements().is_empty());
806        assert!(AnsiDialect.sid_prerequisite_statements().is_empty());
807    }
808}