razeri-plugin-contract 0.21.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
# razeri-plugin-contract

The contract a [Razeri](https://github.com/gaoash/razeri-releases) dialect plugin is written against.

This crate and [`razeri-plugin-api`](https://crates.io/crates/razeri-plugin-api) are the two open crates in Razeri's workspace. Everything a plugin author needs in order to *describe* and *render* a database platform lives here; nothing else does. It depends on `serde` and on nothing else at all, which is what lets a plugin be built, published and consumed outside Razeri's repository.

## What is in here

**The rendering hierarchy** — ten traits, layered so that a new dialect implements four required methods and inherits ANSI-standard behaviour for the rest:

| Trait | What it renders |
|-------|-----------------|
| `SqlRenderer` | Core SQL — the four required methods |
| `ExpressionRenderer` | Expressions, with ANSI defaults |
| `DdlRenderer` | DDL, with ANSI defaults |
| `DmlRenderer` | DML, with ANSI defaults |
| `ProcedureRenderer` | Stored procedures and functions |
| `FormulaRenderer` | Formulas, with ANSI defaults |
| `TaskRenderer` | Scheduled tasks |
| `ReplicationRenderer` | Data extraction, loading and key-function deployment |
| `StorageRenderer` | Partitioning, compression and index strategy |
| `DialectCapabilities` | What the platform's SQL can and cannot do |

**The vocabulary those traits speak**: `DataType` and `DataTypeRef`, the `StoragePlan` a `StorageRenderer` translates, the `StoragePlatform` a storage plugin names, and `DialectFamily`, the platform's own identity.

**The decisions a dialect states** rather than a core crate deciding for it — starting with `SidSpelling`, how this engine spells a surrogate key, and continuing with `ProcedureForm`, `ScalarFunctionForm` and `TableFunctionForm`, the routine grammars it speaks. Each of the three carries a `Custom` arm for a grammar Razeri does not know: your dialect names it and renders the routine itself, from the `RoutineSpec` Razeri hands over.

**The spelling a source gives a day** — `date_format`. One import arrives writing `1/1/2015` in one column and `2015-01-01T00:00:00Z` in the next, and no two engines read either of them the same way. The design states the spelling, this crate reads it once into a list of directives, and `ExpressionRenderer::render_parse_datetime` is where your dialect translates that list into whatever pattern language your engine has. A directive your engine cannot say is a refusal naming it, never a guess.

**The way a source writes an amount** — `number_format`. The same file writes a price `$2,899.99 `, and the symbol, the thousands mark and the trailing space are not part of the number. The design states the picture, this crate reads it once into the marks that are not part of the value, and `ExpressionRenderer::render_parse_number` is where your dialect converts what is left. Three shared shapes are here for the three kinds of engine: one with a conversion that answers nothing, one that needs a shape guard in front of one that does not, and one with neither.

**The questions asked of a warehouse that is already there** — `SqlRenderer::render_catalog_columns_query` and `render_catalog_object_times_query`. Razeri can read a design and say what it would build; how much of a running warehouse it actually built is a question only that warehouse answers. So your dialect spells the two queries that ask it — one row per column, and the moment each object was created where your engine records one — and `catalog_system_schemas` names the schemas a scan of the whole warehouse passes over. The standard's `information_schema` is what the first one answers by default, and `None` is a complete answer to either: an engine that keeps no catalog Razeri can read is named, not reported as an empty warehouse.

**The fan-out an engine states** — `flatten`. One row holding an array becomes one row per element, and no two engine families spell that the same way, so the dialect's `render_flatten` answers with the whole shape — the `FROM` item, the ordinal and one expression per field — rather than filling in a template. The ordinal it returns counts from one on every engine, because the element's surrogate key is derived from its position.

**The surrogate key's composition** — `sid::sid_expression` turns a `SidSpelling` into the key expression an engine runs. It is here because a dialect author who cannot render their own key expression cannot run it either, and *"the same key on every engine"* is then a claim nobody outside Razeri's repository can check.

`AnsiDialect` is a complete worked implementation of the required methods, and `render_data_type_ansi` is the ANSI type mapping to start from.

## How this crate is versioned

While the major version is zero:

- **a change to a trait's method set bumps the minor version** — a method added, removed or renamed, whether or not it carries a default body;
- **documentation and fixes bump the patch version.**

The rule is about who decides when a plugin's behaviour changes. Cargo flows a patch release to every plugin pinned at the minor the next time it builds, and flows a minor release to nobody. A defaulted method costs an existing plugin no line of code, and it still changes what that plugin *says*: an engine that has not implemented the new method now answers the default, and its users meet whatever Razeri does with that answer — a refusal they had never seen, arriving because somebody else released. So a new method is opted into, and the plugin author chooses the day.

This is stricter than semantic versioning requires of a zero-major crate, deliberately: the looser reading saves a version number and spends a plugin author's afternoon.

### What 0.5.0 adds to the method set

- `StorageRenderer::render_partition_retain_procedure` takes the companion table a load sets rows aside in, so one call to a retention procedure reaches both tables.
- `ExpressionRenderer::render_parse_datetime` renders the parse of a value the source spells its own way into a date or a timestamp. Leave it alone and your engine states nothing: a design naming a spelling is refused, with your engine named.
- `SqlRenderer::delimited_file_relation_in_encoding` reads a delimited file whose bytes are not UTF-8. Leave it alone and a source naming a fallback encoding is declined, with your engine named, rather than read as some other language.

### What 0.6.0 adds to the method set

- `SqlRenderer::render_catalog_columns_query` asks a running warehouse what it holds, one row per column. Leave it alone and you get the standard's `information_schema`, which most engines carry; answer `None` where yours has no catalog to read.
- `SqlRenderer::render_catalog_object_times_query` asks when each object was created. Leave it alone and the answer is `None`, which is the truthful one on every engine that records no such moment.
- `SqlRenderer::catalog_system_schemas` names the schemas a scan of a whole warehouse passes over. Leave it alone and it is `information_schema` alone; add the others your engine keeps its own bookkeeping in.
- `SqlRenderer::catalog_scope_predicate` is the `WHERE` clause both catalog queries share — one named schema, or every schema that is not your engine's own. Leave it alone unless your catalog wants its scope written differently.
- `ExpressionRenderer::render_parse_number` renders the reading of a value the source writes its own way into a number. Leave it alone and your engine states nothing: a number column naming a picture is refused, with your engine named.
- `StorageRenderer::slice_replacement_is_procedure_body` says whether the slice replacement you rendered runs on its own or only inside a procedure. Leave it alone and Razeri reads the shape of your answer — a block opened by `IF` and closed by `END IF` is taken for a procedure body — which is a guess your engine can settle.

### What 0.11.0 adds to the method set

- `DialectCapabilities::server_version_query` states how to ask a running server its version: one row whose first word is the version, in the spelling your dialect declares. Leave it alone and nothing is asked: a deploy script for your engine sends no question.
- `DialectCapabilities::parse_server_version` reads the version out of that answer. Leave it alone and the first word is read, which is what a deploy script reads too.
- `ExpressionRenderer::render_read_only_refusal` writes a statement that reads a table, changes nothing, and stops with a sentence where a row meets a condition. Leave it alone and it is `None`: a load's refusal stays a statement that would write, stopped by `render_refusal`, as before.
- `ProcedureRenderer::render_call_for_each_row` writes a statement that calls a procedure once for every row of a query. Leave it alone and it is `None`: a fill that has to make a partition for every day a file holds before staging it is refused on your engine, by name, with the calls to make by hand.

Beside the method set, `EngineVersion` is new: a version of an engine, read from `17`, `8.4`, `2022` or `23ai`, which `razeri-plugin-api`'s `DialectInfo::versions` lists. `split_platform` reads `postgres@17` into the engine and the version.

Beside the 0.6.0 method set, `sid::normalize_trim` is now public: the `TRIM` in your engine's own spelling, which is where reading a written amount starts.

### What 0.18.0 changes

- Thirteen facts about the statements an engine takes are stated once, on the renderer: ten on `SqlRenderer` (`supports_on_conflict`, `supports_update_from`, `merge_not_matched_keyword` and the rest) and three on `DdlRenderer` (`supports_cascade_drop`, `supports_sync_drop`, `supports_force_drop`). `DialectCapabilities` no longer declares them; it requires `DdlRenderer` and reads them there, so the two can never answer differently.
- Delete those thirteen from your `impl DialectCapabilities` and keep each answer on the renderer. A test double that implemented `DialectCapabilities` alone adds the four required `SqlRenderer` methods and an empty `impl DdlRenderer`. The `CHANGELOG.md` in this package says more.
- `DdlRenderer::render_copy_table` is new: the statement that copies a table, every row it holds, into a new table beside it, which Razeri writes before a change destroys data when the operator asks to keep a copy first. The default is `CREATE TABLE … AS SELECT * FROM …`, with the clause your `render_table_suffix` answers for a plain table before `AS`; an engine with no `CREATE TABLE … AS` answers in its own words.

### What 0.17.0 changes

- A delimited file tells your dialect how it is written: `DelimitedFile` carries the `delimiter` (a `FileDelimiter`: a comma, a tab, a pipe, a semicolon, one of the four ASCII separators, or any other character), the `quote`, whether the first line is a `header`, and the `encoding` its bytes are read as. Put them where your engine's read takes them, and answer `None` for a file your engine cannot be told about.
- The default `SqlRenderer::delimited_file_relation_for` reads only a plain file — split on commas, quoted with `"`, headed — and answers `None` for any other, so Razeri declines that file by name rather than reading it as one column.
- `DelimitedFile` is `#[non_exhaustive]`: a test builds one with `DelimitedFile::new` and the `with_` methods. A dialect that only reads it compiles unchanged. The `CHANGELOG.md` in this package says more.

### What 0.16.0 changes

- A view other objects read is replaced on an engine that will not replace it in place: answer `false` to `DdlRenderer::replaces_a_view_that_is_read`, and Razeri hands each view the objects that read it, which `DdlRenderer::render_view_replacement` drops first.
- `SqlRenderer::is_reserved_word` says which words your engine reserves, the SQL standard's by default, and Razeri quotes the short names it writes inside a statement where the word is reserved.
- `FormulaRenderer::supports_split`, `supports_regexp_like`, `supports_regexp_extract` and `supports_ordered_string_agg` let a dialect say its engine lacks a function, and a formula that needs it is refused by name.
- `ColumnEvolution::RequireColumn` is the step that requires a column once every row holds a value.
- Every change is additive: a dialect that compiled against 0.15.0 compiles unchanged. The `CHANGELOG.md` in this package says more.

### What 0.15.0 changes

- A load opens its own day's partitions: `StorageRenderer::partition_extend_call` answers an `ExtendCall`, which says whether the call runs inside the load or before its transaction opens. It replaces `partition_extend_call_before_write`.
- `EntityRef` carries a `TableRole` — written, landing or identity — in place of `is_identity_table`.
- `DialectCapabilities::type_limits` states the digits of a second and the fixed lengths an engine holds.
- A time says whether its digits of a second were stated: `TimeDataType::precision` is an `Option<u8>`, and `timestamp(0)` and `time(0)` are whole seconds.
- The number readings ask the shape in flat tests and take a `ClassTest`; every reading asks for a digit before the first thousands mark.
- The `CHANGELOG.md` in this package says, change by change, what a dialect changes to compile.

### What 0.14.0 changes

- The surrogate key is the hash of the value as stored: `sid::sid_expression` and its kin no longer fold each part, and a composite escapes each part before the join. They take a `sid::Backslash`, read with `Backslash::of(&your_dialect)`.
- What Razeri used to decide by an engine's name is asked of the dialect, each with a default that keeps today's answer: `bulk_load_form`, `render_refresh_statistics`, `update_from_takes_a_source_naming_the_targets_columns`, `render_view_options`, `render_join_options`, `render_table_storage_options`, `render_drop_column`, `render_drop_view`, `render_create_table_if_absent`, `EvolutionAnswers::drops_a_column_in_place_before_an_index_column`, `begin_leaves_the_commit_to_the_client`, `catalogue_reach`, `render_comparable`, `text_length_unit` and `render_bitwise_operator`.
- `keeps_a_time_zone` says whether an engine has a type that keeps a time zone.
- `text_limits` states the longest `varchar` an engine takes, and `render_not_an_iso_day` how an engine that holds a date as text finds one not written as an ISO date.
- The number readers for an engine with no pattern match hold a value to the picture's shape before its marks come out, so one picture reads the same values on every engine.
- `render_create_task`, `render_bulk_load` and `render_procedure_log` answer `Option<String>`: `None` is how an engine with no scheduler, no load statement or no log statement says so. `TaskRenderConfig` carries `enabled`.
- The `CHANGELOG.md` in this package says, change by change, what a dialect changes to compile.

### What 0.13.0 changes

- `UpsertForm::OnConflictDoUpdate` is new: an engine with no `MERGE` whose upsert is `INSERT … ON CONFLICT … DO UPDATE` answers `MergeShape::Upsert(UpsertForm::OnConflictDoUpdate)`, and Razeri writes its core loads in that statement.
- `SidSpelling::ascii_fold` is new: `AsciiFold::Replacements` by default, or `AsciiFold::Functions` naming the functions your engine holds that change the case of `a` to `z` and nothing else. A spelling built field by field adds the field; one ending `..ANSI_SID_SPELLING` is unchanged.
- `routine_render::render_table_function_as` writes `DROP FUNCTION IF EXISTS <name>(<input types>);` above a replaced `PostgreSQL` function, which the engine will not replace when its returned columns changed.
- `routine_render::render_procedure_as` takes your dialect's `supports_procedure_comment()` as its third argument, and leaves the description out where it is `false`.

### What 0.12.0 changes

- `SqlRenderer::fold_schema_into_name` is new: on an engine with no schemas, the name an object takes when the schema is folded into it. Leave it alone and the schema is dropped, as before, and a design whose layers share a table name is refused.
- `FormulaRenderer::render_power_operator`, `render_ilike_operator`, `render_not_ilike_operator` and `render_concat_operator` answer `Option<String>`: `None` says the engine has no such operator, and a formula that needs it is refused by name. A dialect that overrides any of them changes its signature; the `CHANGELOG.md` in this crate's package says how.
- `Concat::Operator` joins a key's parts with an operator: `Concat::Operator("||")`.

## Stability

This crate and `razeri-plugin-api` follow semantic versioning while the
major version is zero, held to the stricter reading above. A trait gains a
method only with a default body, so a new method costs an existing plugin
no line of code. A change that does break a plugin's build — a method whose
answer changes type, an enum a `match` has to name a new variant of — comes
only in a minor release, and the changelog says what to change. A method
that leaves does so as `#[deprecated]` for one minor version before it is
removed, so a plugin sees the warning before the removal, not after.

## What is deliberately not in here

Razeri's design model and every renderer that produces Razeri's own output. A plugin says what its engine spells; Razeri composes.

## License

Dual-licensed under [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE), at your option. Plugins belong to their authors.

## Security

Report a vulnerability as Razeri's [security policy](https://github.com/gaoash/razeri-releases/blob/main/SECURITY.md) says.