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