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