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