razeri_plugin_contract/lib.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#![warn(missing_docs)]
6
7//! # razeri-plugin-contract
8//!
9//! **The contract a Razeri dialect plugin is written against.**
10//!
11//! This crate and `razeri-plugin-api` are the two open crates in Razeri's
12//! workspace. Everything a plugin author needs in order to *describe* and
13//! *render* a database platform lives here; nothing else does. It depends on
14//! `serde` and on nothing else at all, which is what lets a plugin be built,
15//! published and consumed outside this repository.
16//!
17//! ## What is in here
18//!
19//! **The rendering hierarchy** — ten traits, layered so that a new dialect
20//! implements four required methods and inherits ANSI-standard behaviour for
21//! the rest:
22//!
23//! 1. [`SqlRenderer`] — core SQL rendering (the four required methods)
24//! 2. [`ExpressionRenderer`] — expressions, with ANSI defaults
25//! 3. [`DdlRenderer`] — DDL, with ANSI defaults
26//! 4. [`DmlRenderer`] — DML, with ANSI defaults
27//! 5. [`ProcedureRenderer`] — stored procedures and functions
28//! 6. [`FormulaRenderer`] — formulas, with ANSI defaults
29//! 7. [`TaskRenderer`] — scheduled tasks
30//! 8. [`ReplicationRenderer`] — data extraction, loading and key-function deployment
31//! 9. [`StorageRenderer`] — partitioning, compression and index strategy
32//! 10. [`DialectCapabilities`] — what the platform's SQL can and cannot do
33//!
34//! **The vocabulary those traits speak**: [`DataType`] and [`DataTypeRef`], the
35//! [`storage_strategy::StoragePlan`] a [`StorageRenderer`] translates, the
36//! [`StoragePlatform`] a storage plugin names, and [`DialectFamily`], the
37//! platform's own identity.
38//!
39//! **The spelling a source gives a day** — [`date_format`]. One import
40//! arrives with `1/1/2015` in one column and `2015-01-01T00:00:00Z` in the
41//! next; the design states the spelling, this crate reads it once into a list
42//! of directives, and each engine translates that list into its own pattern
43//! language through [`ExpressionRenderer::render_parse_datetime`]. A directive
44//! an engine cannot spell is refused by name.
45//!
46//! **The way a source writes an amount** — [`number_format`]. A file writes a
47//! price `$2,899.99 `, where the symbol, the thousands mark and the space
48//! around it are not part of the number. The design states the picture, this
49//! crate reads it once into the marks that are not part of the value, and each
50//! engine takes those marks out and converts what is left through
51//! [`ExpressionRenderer::render_parse_number`]. An engine that states nothing
52//! is refused by name rather than guessed for.
53//!
54//! **The questions asked of a warehouse that is already there** —
55//! [`SqlRenderer::render_catalog_columns_query`] and
56//! [`SqlRenderer::render_catalog_object_times_query`]. A design says what
57//! would be built; how much of a running warehouse was built is a question
58//! only that warehouse answers, so each engine spells the two queries that
59//! ask it and names the schemas a whole-warehouse scan passes over through
60//! [`SqlRenderer::catalog_system_schemas`]. `None` is a complete answer to
61//! either query: an engine with no catalog to read is named rather than
62//! reported as an empty warehouse.
63//!
64//! **The version of an engine** — [`EngineVersion`]. A platform is an engine
65//! at a version: `postgres`, or `postgres@17`. A dialect declares the versions
66//! it answers for, and states how to ask a running server its version through
67//! [`DialectCapabilities::server_version_query`], so a deploy script can refuse
68//! a server older than the bundle before anything runs.
69//!
70//! **The fan-out an engine states** — [`flatten`]. One row holding an array
71//! becomes one row per element, and every engine spells that differently, so
72//! the dialect answers with the whole shape rather than filling in a template.
73//!
74//! **The decisions a dialect states rather than a core crate deciding for it**,
75//! starting with [`SidSpelling`] — how this engine spells a surrogate key —
76//! and continuing with [`ProcedureForm`] and [`ScalarFunctionForm`], the
77//! routine grammars it speaks, [`MergeShape`], how it upserts, and
78//! [`EvolutionAnswers`], what it keeps when a table that already holds rows
79//! changes shape.
80//!
81//! **The surrogate key's composition** — [`sid`] turns a [`SidSpelling`] into
82//! the key expression an engine runs. It is here rather than in the product
83//! because a dialect author who cannot render their own key expression cannot
84//! run it either, and *"the same key on every engine"* is then a claim nobody
85//! outside this repository can check.
86//!
87//! **The named routine grammars, spelled** — [`routine_render`]. A dialect
88//! whose engine has stored procedures in a grammar of its own renders the whole
89//! `CREATE` itself, and *"PL/pgSQL with one tweak"* need not mean writing the
90//! whole thing from nothing. These three functions write out the grammar a
91//! dialect is nearly the same as, so it can start there and say what differs.
92//!
93//! ## What is deliberately not in here
94//!
95//! Razeri's design model, and the renderers that turn it into Razeri's own
96//! output. A plugin says what its engine spells; Razeri composes. That division
97//! is why one contract crate can be open while the product is not.
98//!
99//! The routine grammars are the one thing on both sides of that line, and they
100//! are here because they belong to the engines rather than to Razeri's design: a
101//! plugin author who cannot read the grammar next to theirs has to guess at it.
102//!
103//! ## How this crate is versioned
104//!
105//! While the major version is zero:
106//!
107//! - **a change to a trait's method set bumps the minor version** — a method
108//! added, removed or renamed, whether or not it carries a default body;
109//! - **documentation and fixes bump the patch version.**
110//!
111//! The rule is about who decides when a plugin's behaviour changes. Cargo
112//! flows a patch release to every plugin pinned at the minor the next time it
113//! builds, and flows a minor release to nobody. A defaulted method costs an
114//! existing plugin no line of code, and it still changes what that plugin
115//! *says*: an engine that has not implemented the new method now answers the
116//! default, and its users meet whatever Razeri does with that answer — a
117//! refusal they had never seen, arriving because somebody else released. So a
118//! new method is opted into, and the plugin author chooses the day.
119//!
120//! This is stricter than semantic versioning requires of a zero-major crate,
121//! deliberately: the looser reading saves a version number and spends a
122//! plugin author's afternoon.
123//!
124//! ### What 0.5.0 adds to the method set
125//!
126//! - [`StorageRenderer::render_partition_retain_procedure`] takes the
127//! companion table a load sets rows aside in, so one call to a retention
128//! procedure reaches both tables.
129//! - [`ExpressionRenderer::render_parse_datetime`] renders the parse of a
130//! value the source spells its own way into a date or a timestamp.
131//! - [`SqlRenderer::delimited_file_relation_in_encoding`] reads a delimited
132//! file whose bytes are not UTF-8, where the engine states a way to.
133//!
134//! ### What 0.6.0 adds to the method set
135//!
136//! - [`SqlRenderer::render_catalog_columns_query`] asks a running warehouse
137//! what it holds, one row per column.
138//! - [`SqlRenderer::render_catalog_object_times_query`] asks when each object
139//! was created, where the engine records it at all.
140//! - [`SqlRenderer::catalog_system_schemas`] names the schemas a
141//! whole-warehouse scan passes over, and
142//! [`SqlRenderer::catalog_scope_predicate`] is the `WHERE` clause both
143//! catalog queries share.
144//! - [`ExpressionRenderer::render_parse_number`] renders the reading of a
145//! value the source writes its own way into a number.
146//! - [`StorageRenderer::slice_replacement_is_procedure_body`] says whether the
147//! slice replacement this engine rendered runs on its own or only inside a
148//! procedure. Left alone, the shape of the answer is read instead.
149//!
150//! ### What 0.11.0 adds to the method set
151//!
152//! - [`DialectCapabilities::server_version_query`] states how to ask a
153//! running server its version, and
154//! [`DialectCapabilities::parse_server_version`] reads the answer.
155//! - [`ExpressionRenderer::render_read_only_refusal`] stops with a sentence
156//! from a statement that only reads, where the engine has one.
157//! - [`ProcedureRenderer::render_call_for_each_row`] calls a procedure once
158//! for every row of a query, where the engine has a statement that loops.
159//!
160//! Beside the method set, [`EngineVersion`] is new: a version of an engine,
161//! read from `17`, `8.4`, `2022` or `23ai`, and [`split_platform`] reads
162//! `postgres@17` into the engine and the version.
163//!
164//! Beside the 0.6.0 method set, [`sid::normalize_trim`] is now public: the `TRIM` in
165//! an engine's own spelling, which the reading of a written amount starts from.
166//!
167//! ### What 0.18.0 changes
168//!
169//! - [`DdlRenderer::render_copy_table`] copies a table, every row it holds,
170//! into a new table beside it: `CREATE TABLE … AS SELECT * FROM …` by
171//! default, with the clause [`SqlRenderer::render_table_suffix`] answers
172//! for a plain table before `AS`.
173//!
174//! ### What 0.17.0 changes
175//!
176//! - [`DelimitedFile`] carries how a file is written — its `delimiter`, a
177//! [`FileDelimiter`], its `quote`, its `header` and its `encoding` — and
178//! is built with [`DelimitedFile::new`] and the `with_` methods.
179//! - The default [`SqlRenderer::delimited_file_relation_for`] reads only a
180//! plain file ([`DelimitedFile::is_plain`]) and answers `None` for any
181//! other.
182//!
183//! ### What 0.16.0 adds to the method set
184//!
185//! - [`DdlRenderer::replaces_a_view_that_is_read`] and
186//! [`DdlRenderer::render_view_replacement`], with [`ViewReplacement`],
187//! [`ViewReader`] and [`ReaderKind`]: a view other objects read, on an
188//! engine that will not replace it in place.
189//! - [`SqlRenderer::is_reserved_word`] and [`SqlRenderer::render_alias`],
190//! over [`reserved_words`]: a short name Razeri writes inside a statement,
191//! quoted where the engine reserves the word.
192//! - [`FormulaRenderer::supports_split`],
193//! [`FormulaRenderer::supports_regexp_like`],
194//! [`FormulaRenderer::supports_regexp_extract`] and
195//! [`FormulaRenderer::supports_ordered_string_agg`]: a function the engine
196//! lacks, refused by name.
197//! - [`ColumnEvolution::RequireColumn`]: requiring a column once every row
198//! holds a value.
199//!
200//! ### What 0.15.0 changes in the method set
201//!
202//! - [`StorageRenderer::partition_extend_call`] replaces
203//! `partition_extend_call_before_write`, answering an [`ExtendCall`] that
204//! says where the call runs ([`ExtendRuns`]).
205//! - [`EntityRef::role`] replaces `is_identity_table`, a [`TableRole`].
206//! - New, with a default: [`DialectCapabilities::type_limits`], answering
207//! [`TypeLimits`].
208//! - [`data_type::TimeDataType::precision`] is an `Option<u8>`, and a
209//! timestamp's 0 is whole seconds.
210//! - [`number_format::read_with_safe_cast`] and
211//! [`number_format::read_with_digit_check`] take a
212//! [`number_format::ClassTest`], and read flat.
213//!
214//! ### What 0.14.0 changes in the method set
215//!
216//! - The key's composers hash the value as stored, and a composite escapes
217//! each part: [`sid::sid_expression`] takes a [`sid::Backslash`].
218//! - New, each with a default: [`SqlRenderer::bulk_load_form`],
219//! [`SqlRenderer::render_refresh_statistics`],
220//! [`SqlRenderer::update_from_takes_a_source_naming_the_targets_columns`],
221//! [`SqlRenderer::render_view_options`], [`SqlRenderer::render_join_options`],
222//! [`DdlRenderer::render_table_storage_options`],
223//! [`DdlRenderer::render_drop_column`], [`DdlRenderer::render_drop_view`],
224//! [`DdlRenderer::render_create_table_if_absent`],
225//! [`DialectCapabilities::begin_leaves_the_commit_to_the_client`],
226//! [`DialectCapabilities::catalogue_reach`],
227//! [`DialectCapabilities::text_limits`],
228//! [`DialectCapabilities::keeps_a_time_zone`],
229//! [`ExpressionRenderer::render_not_an_iso_day`],
230//! [`ExpressionRenderer::render_comparable`],
231//! [`ExpressionRenderer::text_length_unit`] and
232//! [`FormulaRenderer::render_bitwise_operator`].
233//! - [`TaskRenderer::render_create_task`], [`TaskRenderer::render_bulk_load`]
234//! and [`SqlRenderer::render_procedure_log`] answer `Option<String>`.
235
236/// This crate's version, as a plugin was compiled against it.
237///
238/// A plugin loaded from a directory is refused unless it was built against
239/// the same version, from the same source, as the razeri that loads it.
240pub const VERSION: &str = env!("CARGO_PKG_VERSION");
241
242/// A fingerprint of this crate's source. Two builds carrying the same version
243/// over different source have different fingerprints.
244pub const SOURCE_FINGERPRINT: u64 =
245 match u64::from_str_radix(env!("RAZERI_PLUGIN_CONTRACT_SOURCE"), 16) {
246 Ok(fingerprint) => fingerprint,
247 Err(_) => panic!("the build script writes the fingerprint in hexadecimal"),
248 };
249
250pub mod agnostic;
251pub mod bulk_load_form;
252pub mod capabilities;
253pub mod constraint_enforcement;
254pub mod data_type;
255pub mod date_format;
256pub mod dialect_family;
257pub mod engine_version;
258pub mod evolution;
259pub mod flatten;
260pub mod formula_renderer;
261pub mod large_object;
262pub mod merge_shape;
263pub mod number_format;
264pub mod procedure_log_level;
265pub mod renderer;
266pub mod replication;
267pub mod reserved_words;
268pub mod routine_form;
269pub mod routine_render;
270pub mod routine_spec;
271pub mod sid;
272pub mod sid_spelling;
273pub mod storage_platform;
274pub mod storage_renderer;
275pub mod storage_strategy;
276pub mod task_renderer;
277pub mod value_text;
278pub mod view_replacement;
279
280pub use agnostic::*;
281pub use bulk_load_form::BulkLoadForm;
282pub use capabilities::*;
283pub use constraint_enforcement::*;
284pub use data_type::{DataType, DataTypeRef};
285pub use date_format::{DateFormat, DateFormatError, FormatDirective, FormatPart};
286pub use dialect_family::DialectFamily;
287pub use engine_version::{EngineVersion, EngineVersionError, split_platform};
288pub use evolution::*;
289pub use flatten::{Flatten, FlattenField, FlattenRequest};
290pub use formula_renderer::*;
291pub use large_object::{HeldAs, TextLengthUnit};
292pub use merge_shape::*;
293pub use number_format::{Negatives, NumberFormat, NumberFormatError};
294pub use procedure_log_level::ProcedureLogLevel;
295pub use renderer::*;
296pub use replication::*;
297pub use routine_form::*;
298pub use routine_render::{
299 render_procedure_as, render_scalar_function_as, render_table_function_as,
300};
301pub use routine_spec::*;
302pub use sid::sid_expression;
303pub use sid_spelling::*;
304pub use storage_platform::StoragePlatform;
305pub use storage_renderer::*;
306pub use task_renderer::*;
307pub use value_text::{
308 CanonicalText, DOUBLE_DIGITS, FLOAT_TEXT_FUNCTION, FloatArithmetic, FloatParts, SINGLE_DIGITS,
309 SINGLE_FLOAT_TEXT_FUNCTION, TextFunctions, UTC_OFFSET, ValueTextFunctions, computed_float_text,
310 float_text, float_text_function, in_short_lines, rtrim_fraction, signed_float_text, truth_text,
311};
312pub use view_replacement::{ReaderKind, ViewReader, ViewReplacement};