///| SQL dialect rules for the four KingbaseES compatibility modes.
///|
///| KingbaseES fixes its dialect at `initdb` time and reports it in the
///| `database_mode` GUC. Measured on the target instance:
///|
///| ```text
///| select vartype, context, enumvals from sys_settings where name='database_mode'
///| --> enum | internal | {pg,oracle,mysql,sqlserver,0,1,2,3}
///| ```
///|
///| The context is `internal`, so no session can change it. A client reads it
///| once and adapts.
///|
///| The mode is not the whole story. Three more GUCs change the same behavior
///| across instances, so this package reads them instead of assuming a value per
///| mode:
///| - `enable_ci` -- identifier case handling, fixed at `initdb`.
///| - `ora_input_emptystr_isnull` -- whether an empty string input means NULL.
///| - `quoted_identifier` -- whether double quotes mark an identifier or a
///| string, on modes that follow SQL Server quoting rules.
///|
///| Every rule carries one of three evidence tags:
///| - `measured`: run on the target instance (`database_mode = sqlserver`),
///| recorded in `docs/data/mode-probe-sqlserver.txt`.
///| - `pg-compatible`: standard PostgreSQL behavior, which `pg` mode reproduces.
///| - `documented`: taken from the KingbaseES manual for that mode, not run
///|
/// against a server. Verify with `kingbase_bench probe` before you rely on it.
pub(all) enum Mode {
Pg
Oracle
Mysql
Sqlserver
} derive(Debug, Eq)
///| The dot-calls that derived traits used to attach implicitly. Neither is used by
///| this module or was ever part of its published interface, and the implicit
///| promotion is on its way out of the language, so each is declared explicitly and
///|
/// marked legacy: call `Debug::to_repr(mode)` or use `==` instead.
#deprecated("Use `Debug::to_repr(mode)` instead", skip_current_package=true)
#doc(hidden)
pub extend Mode with Debug::{to_repr}
///|
#deprecated("Use `mode == other` instead", skip_current_package=true)
#doc(hidden)
pub extend Mode with Eq::{not_equal, equal}
///| The mode assumed when an instance reports nothing.
///|
/// `pg` is the base every KingbaseES build keeps, because KingbaseES is built on
/// the PostgreSQL kernel, so an unreadable mode is closest to that.
pub fn unknown_mode() -> Mode {
Pg
}
///|
/// Maps the value of `database_mode` to a `Mode`.
///|
/// The GUC also reports `0` to `3`, so both spellings are accepted.
pub fn mode_of(name : String) -> Mode? {
match name {
"pg" | "postgres" | "postgresql" | "0" => Some(Pg)
"oracle" | "1" => Some(Oracle)
"mysql" | "2" => Some(Mysql)
"sqlserver" | "mssql" | "sql_server" | "3" => Some(Sqlserver)
_ => None
}
}
///|
/// The short name the server uses for this mode.
pub fn Mode::name(self : Mode) -> String {
match self {
Pg => "pg"
Oracle => "oracle"
Mysql => "mysql"
Sqlserver => "sqlserver"
}
}
///| The setting names a `Dialect` reads.
///|
/// One query asks for all of them. `sqlserver` mode treats a double quote as a
/// string delimiter unless `quoted_identifier` is on, and an apostrophe as an
/// escape, so a name list built with string concatenation can fail to parse on
/// one mode and work on another. The query is therefore written once, as the
/// bracket-quoted form in `sql_in_list`, and this list only names the keys.
/// measured in `docs/data/probe-sqlserver.txt`.
pub fn setting_names() -> Array[String] {
[
"database_mode", "server_version", "enable_ci", "ora_input_emptystr_isnull",
"quoted_identifier", "sql_mode", "datestyle", "dateformat", "copy_mode", "standard_conforming_strings",
]
}
///|
/// The query that reads every name in `setting_names` from one catalog view.
///|
/// The names are single-quoted, which is the form every mode takes; the view
/// name is the caller's choice, because it differs per mode. On another mode the
/// query may fail to parse, and `Client::connect` then falls back to `show` per
/// name, so a mode with no such view still gets its dialect.
pub fn sql_in_list(view : String) -> String {
// 39 is the apostrophe, written as a code so the MoonBit lexer sees no
// nested quote.
let q = quote_code(39)
let names = setting_names().map(n => q + n + q).join(", ")
"select name, setting from " +
view +
" where name in (" +
names +
") order by 1"
}
///|
/// The catalog views to try when reading settings, best guess first.
///|
/// measured across all four modes: `pg` mode has no `sys_*` view at all
/// (`sys_settings` answers 42P01) and exposes `pg_settings`; `sqlserver`,
/// `oracle` and `mysql` answer `sys_settings`. The second name is a fallback for
/// a build that reports its mode differently from the catalogs it ships.
pub fn settings_views(mode : Mode) -> Array[String] {
match mode {
Pg => ["pg_settings", "sys_settings"]
Oracle | Mysql | Sqlserver => ["sys_settings", "pg_settings"]
}
}
///|
/// The name this mode uses for one catalog object, e.g. `class` or `indexes`.
///|
/// measured: pg mode answers `pg_class`, `pg_namespace`, `pg_indexes` and
/// rejects `sys_class` with 42P01; sqlserver mode answers both prefixes, so the
/// `sys_` form serves the other three modes.
pub fn Dialect::catalog_view(self : Dialect, base : String) -> String {
if self.mode == Pg {
"pg_" + base
} else {
"sys_" + base
}
}
///|
/// One character as a one-character string.
///|
/// A character literal is a `Char`, and a `Char` cannot be appended to a
/// `String` directly, so an escape is turned into a string first.
///|
/// `unsafe_to_char` is used because every call site passes an ASCII code; the
/// safe `to_char` returns `Char?` and would only add a check that cannot fail.
fn quote_code(code : Int) -> String {
String::from_array([code.unsafe_to_char()])
}
///|
/// One setting name, folded to lower case for the keys of a `Dialect`.
///|
/// The catalog reports `DateStyle` and `DateFormat` while the client asks for
/// `datestyle` and `dateformat`, so keys must be normalised. Only ASCII letters
/// are folded, because a locale-dependent fold would change per session.
fn fold_key(name : String) -> String {
let out : Array[Char] = []
for ch in name.iter() {
let c = ch.to_int()
if c >= 65 && c <= 90 {
out.push((c + 32).unsafe_to_char())
} else {
out.push(ch)
}
}
String::from_array(out)
}
///|
/// One SQL term a schema builder needs, mapped to a type name per mode.
pub(all) enum ColumnKind {
BigInt
Int32
Int16
/// precision and scale
Decimal(Int, Int)
/// maximum length
Varchar(Int)
/// fixed length
Char(Int)
Timestamp
Boolean
}
///|
/// How an unquoted identifier appears in the catalog afterwards.
pub(all) enum CaseFold {
/// pg-compatible, and documented for `enable_ci = on`
Lower
/// documented for Oracle mode
Upper
/// measured: `select 1 as Foo` reports the column name `Foo`
Preserve
} derive(Debug, Eq)
///| Dot-calls from the derived traits above, declared for the same reason as
///| `Mode`'s: `assert_eq` on a `CaseFold` needs `Debug` and `Eq`, and the implicit
///|
/// promotion that used to attach these is deprecated.
#deprecated("Use `Debug::to_repr(fold)` instead", skip_current_package=true)
#doc(hidden)
pub extend CaseFold with Debug::{to_repr}
///|
#deprecated("Use `fold == other` instead", skip_current_package=true)
#doc(hidden)
pub extend CaseFold with Eq::{not_equal, equal}
///| The dialect of one connected instance: its mode plus the settings that
///|
/// change the answer.
///|
/// Construct it with `new_dialect`. The values are measured on the
/// live instance in `Client::connect`, so they reflect the target, not a guess
/// about a mode.
pub struct Dialect {
mode : Mode
settings : Map[String, String]
}
///|
/// Reads the settings a `Dialect` needs from rows of `(name, value)`.
pub fn new_dialect(
rows : Array[(String, String)],
/// used when the target does not report `database_mode`
fallback : Mode,
) -> Dialect {
let m = Map([])
for pair in rows {
let (k, v) = pair
m[fold_key(k)] = v
}
let mode = match m.get("database_mode") {
Some(s) =>
match mode_of(s) {
Some(md) => md
None => fallback
}
None => fallback
}
{ mode, settings: m, }
}
///|
/// The value of one setting, or "" when the instance does not report it.
pub fn Dialect::setting(self : Dialect, key : String) -> String {
match self.settings.get(key) {
Some(v) => v
None => ""
}
}
///|
/// A `on` / `off` setting, with a default for an instance that lacks it.
pub fn Dialect::flag(self : Dialect, key : String, default : Bool) -> Bool {
match self.settings.get(key) {
Some(v) =>
match v {
"on" | "true" | "1" | "yes" => true
"off" | "false" | "0" | "no" => false
_ => default
}
None => default
}
}
///| Does an empty string input mean NULL?
///|
/// The instance answers this. measured on the target:
/// `ora_input_emptystr_isnull = off`, and an empty `varchar` is not NULL there.
///|
/// documented: the manual for that GUC says `on` makes an empty string NULL to
/// match Oracle, and that `pg` mode needs it `off`. When the GUC is absent, the
/// mode decides.
pub fn Dialect::empty_string_is_null(self : Dialect) -> Bool {
if self.settings.contains("ora_input_emptystr_isnull") {
self.flag("ora_input_emptystr_isnull", false)
} else {
match self.mode {
Oracle => true
_ => false
}
}
}
///| How unquoted identifiers are folded.
///|
/// `enable_ci` is read when the instance reports it. documented: case
/// insensitive handling folds names to lower case. measured: this instance
/// reports `enable_ci = on` and keeps the written case in a result column
/// label, so label text is not evidence about the catalog.
pub fn Dialect::case_fold(self : Dialect) -> CaseFold {
if self.settings.contains("enable_ci") {
if self.flag("enable_ci", true) {
Lower
} else {
Preserve
}
} else {
match self.mode {
Pg => Lower
Oracle => Upper
Mysql | Sqlserver => Preserve
}
}
}
///| Quotes one identifier.
///|
/// measured: on this instance `quoted_identifier = on`, `"Foo"` marks an
/// identifier, `[Foo Bar]` also works, and a backtick is a syntax error
/// (SQLSTATE 42601).
///|
/// When `quoted_identifier` is off, double quotes mark a string on modes that
/// follow SQL Server rules, so brackets are used instead. The manual does not
/// cover bracket acceptance, so `probe` measures it.
pub fn Dialect::quote_ident(self : Dialect, name : String) -> String {
let use_backtick = match self.mode {
Mysql => !self.sql_mode_has("ANSI_QUOTES")
_ => false
}
if use_backtick {
"`\{name}`"
} else if self.mode == Sqlserver && !self.flag("quoted_identifier", true) {
"[\{name}]"
} else {
"\"\{name}\""
}
}
///|
/// The literal form of a boolean in an SQL statement, unquoted.
///|
/// This follows the type the mode chooses, like `boolean_text`, but the value must
/// be a bare literal rather than COPY field text. pg and mysql mode get a
/// PostgreSQL `boolean`, whose keywords are `true` and `false`; sqlserver mode gets
/// `bit` and oracle mode `number(1)`, which take `1` and `0`. The `crud` command in
/// the benchmark repository measures which forms each instance accepts.
pub fn Dialect::boolean_literal(self : Dialect, value : Bool) -> String {
if self.column_type(Boolean) == "boolean" {
if value {
"true"
} else {
"false"
}
} else if value {
"1"
} else {
"0"
}
}
///|
/// True when `sql_mode` lists one flag. measured: this instance reports
/// `sql_mode = ONLY_FULL_GROUP_BY,ANSI_QUOTES` even in sqlserver mode.
pub fn Dialect::sql_mode_has(self : Dialect, flag : String) -> Bool {
let s = self.setting("sql_mode")
s.split(",").any(part => part.trim() == flag)
}
///| The type name for one column kind.
///|
/// The names below are the ones a schema builder needs. measured for
/// sqlserver mode: `bigint`, `int`, `smallint`, `numeric(18,4)`, `varchar(n)`,
/// `datetime`, and `bit` all cast without error, while `varchar2`, `number` and
/// `nvarchar2` do not exist there (SQLSTATE 42704).
pub fn Dialect::column_type(self : Dialect, kind : ColumnKind) -> String {
match self.mode {
// pg-compatible: the PostgreSQL type names.
Pg =>
match kind {
BigInt => "bigint"
Int32 => "integer"
Int16 => "smallint"
Decimal(p, s) => "numeric(\{p},\{s})"
Varchar(n) => "varchar(\{n})"
Char(n) => "char(\{n})"
Timestamp => "timestamp"
Boolean => "boolean"
}
// documented: Oracle has no integer types. `number(p)` carries them, and
// the manual says `date` stores date and time, so a timestamp column is
// still the widest choice: `timestamp`.
// measured here: `varchar2` and `number` are absent on the sqlserver
// instance, which matches a mode-scoped type set.
Oracle =>
match kind {
BigInt => "number(19)"
Int32 => "number(10)"
Int16 => "number(5)"
Decimal(p, s) => "number(\{p},\{s})"
Varchar(n) => "varchar2(\{n})"
Char(n) => "char(\{n})"
Timestamp => "timestamp"
Boolean => "number(1)"
}
// documented: MySQL type names. `datetime` is used instead of `timestamp`
// because a MySQL `timestamp` is a 32-bit type with automatic update
// semantics.
// measured: the manual calls `boolean` an alias of `tinyint(1)`, but this
// instance stores a PostgreSQL boolean under that name and prints `t`, so
// `boolean_text` follows the type name rather than the MySQL rule.
Mysql =>
match kind {
BigInt => "bigint"
Int32 => "int"
Int16 => "smallint"
Decimal(p, s) => "decimal(\{p},\{s})"
Varchar(n) => "varchar(\{n})"
Char(n) => "char(\{n})"
Timestamp => "datetime"
Boolean => "boolean"
}
// measured: SQL Server names on this instance.
// `datetime` is required for a timestamp column, because a `timestamp`
// column in this mode is the rowversion type: the cast
// `select cast('2022-01-01 10:20:30' as timestamp)` returns a column named
// `rowversion` and the value `0x323032322D30312D`.
Sqlserver =>
match kind {
BigInt => "bigint"
Int32 => "int"
Int16 => "smallint"
Decimal(p, s) => "numeric(\{p},\{s})"
Varchar(n) => "varchar(\{n})"
Char(n) => "char(\{n})"
Timestamp => "datetime"
Boolean => "bit"
}
}
}
///|
/// The type name this mode uses for a date-and-time column.
pub fn Dialect::timestamp_type(self : Dialect) -> String {
self.column_type(Timestamp)
}
///| The clause that returns `limit` rows starting at `offset`.
///|
/// The modes share no one pagination syntax, so generated SQL asks for a clause
/// instead of writing `limit` in the query text.
/// measured: this sqlserver instance accepts all three of
/// `limit 1 offset 0`, `top 1`, and `offset 0 rows fetch next 1 rows only`.
/// documented: the SQL Server reference says `TOP` cannot combine with `OFFSET`
/// and `FETCH` in one query, so the two forms stay separate here.
pub fn Dialect::limit_clause(
self : Dialect,
limit : Int,
offset : Int,
) -> String {
match self.mode {
// pg-compatible.
Pg => "limit \{limit} offset \{offset}"
// documented: Oracle 12c and later accept the standard form.
Oracle => "offset \{offset} rows fetch next \{limit} rows only"
// documented: the MySQL reference covers `LIMIT row_count`; the
// `LIMIT count OFFSET start` spelling is the MySQL form and is marked
// documented until a mysql-mode run confirms it.
Mysql => "limit \{limit} offset \{offset}"
// measured: the standard form works, and it needs no hidden `order by`.
Sqlserver => "offset \{offset} rows fetch next \{limit} rows only"
}
}
///| Renders "the first `n` rows" as a clause on the `select` keyword.
///|
/// documented: `TOP` belongs to the SQL Server grammar. The other modes return
/// "" here, and the caller uses `limit_clause` with offset 0 instead.
pub fn Dialect::top_clause(self : Dialect, n : Int) -> String {
match self.mode {
// measured: `select top 1 1` returns one row.
Sqlserver => "top \{n} "
Pg | Oracle | Mysql => ""
}
}
///| Renders the concatenation of two expressions.
///|
/// measured: `||` on two `varchar` values fails on this instance with SQLSTATE
/// 42883 ("varchar || varchar" operator does not exist), while `concat()` and
/// `+` both work.
///|
/// pg-compatible: `||` is the standard operator in `pg` mode.
///|
/// documented: the MySQL reference lists `CONCAT`, and in MySQL `||` is a
/// logical operator unless `sql_mode` adds `PIPES_AS_CONCAT`.
pub fn Dialect::concat(self : Dialect, a : String, b : String) -> String {
let pipes = match self.mode {
Pg | Oracle => true
Mysql => self.sql_mode_has("PIPES_AS_CONCAT")
Sqlserver => false
}
if pipes {
"\{a} || \{b}"
} else {
"concat(\{a},\{b})"
}
}
///| Renders the current timestamp.
///|
/// The expression that returns the current date and time in this mode.
/// measured, one instance per mode: `now()` answers in all four. `sysdate` works
/// only in oracle and mysql mode, and `getdate()` — the SQL Server form this mode
/// is named after — is refused in all four. `current_timestamp` answers in all
/// four too, and prints differently: sqlserver mode gives `now()` an offset
/// (`2026-10-09 12:30:55.458090+08`) but `current_timestamp` none, mysql mode
/// gives neither an offset, and pg mode gives both one. A client therefore reads
/// timestamps as text and never parses them, which is what `ResultSet` does.
pub fn Dialect::now_expression(self : Dialect) -> String {
match self.mode {
Pg | Oracle | Mysql | Sqlserver => "now()"
}
}
///| The statement that opens a transaction in this mode.
///|
/// measured, one instance per mode: sqlserver mode refuses bare `begin` with
/// 42601 ("syntax error at end of input"), because there that word starts a
/// `BEGIN ... END` block, and only `begin transaction` — or its SQL Server
/// abbreviation `begin tran`, which the other three modes refuse — opens one. pg,
/// oracle and mysql mode take both `begin` and `begin transaction`.
///|
/// `commit` and `rollback` are accepted bare in all four modes, and the longer
/// `commit transaction` / `rollback transaction` forms are too, so only the
/// opening statement needs a dialect. An application that must run in every mode
/// can send `begin transaction`: measured accepted by all four instances.
pub fn Dialect::begin_statement(self : Dialect) -> String {
match self.mode {
Sqlserver => "begin transaction"
Pg | Oracle | Mysql => "begin"
}
}
///| Can this mode take `COPY ... FROM STDIN` in binary format?
///|
/// measured, one instance per mode: a `PGCOPY` binary image loaded into a session
/// temp table and the value read back was correct in pg, oracle, mysql and
/// sqlserver mode, and `COPY ... TO STDOUT WITH (FORMAT BINARY)` answers the same
/// way. The clause is accepted; an earlier claim here that it returned 42601 came
/// from sending the non-existent option `WITH (BINARY)`.
///|
/// The loader still writes text: its throughput is set by the server's insert
/// path, and text needs no encoder per column type. `probe` re-measures this per
/// instance.
pub fn Dialect::binary_copy_supported(self : Dialect) -> Bool {
match self.mode {
Pg | Oracle | Mysql | Sqlserver => true
}
}
///| Does the mode take the extended query protocol (Parse / Bind / Execute)?
///|
/// measured, one instance per mode: a lone Parse answers SQLSTATE 08P01
/// ("insufficient data left in message") in pg, oracle, mysql and sqlserver mode,
/// whether the messages are framed by this library or by a second, independent
/// implementation, and the session survives it. This is a property of the
/// deployment, not of one compatibility mode.
///|
/// Prepared statements are reachable as SQL over the simple protocol. measured:
/// a parameterised one runs in every mode; a parameterless one runs in pg, oracle
/// and mysql mode, while sqlserver mode reads `execute p` as a stored-procedure
/// call (42883) and `execute p()` as a syntax error (42601).
///|
/// The simple query protocol works in all four modes, so the client uses it and a
/// false value here costs nothing.
pub fn Dialect::extended_protocol_supported(self : Dialect) -> Bool {
match self.mode {
Pg | Oracle | Mysql | Sqlserver => false
}
}
///| Are backslash escapes in a string literal a escape mechanism or a literal?
///|
/// measured: `standard_conforming_strings = on`, and `cast('a\\b' as
/// varchar(10))` keeps the two characters as written.
///|
/// pg-compatible: the same GUC name and meaning.
pub fn Dialect::conforming_strings(self : Dialect) -> Bool {
self.flag("standard_conforming_strings", true)
}
///| Text the server returns for a timestamp value.
///|
/// measured: this instance prints `2022-01-01 10:20:30.000` with
/// `datestyle = ISO, YMD` and `dateformat = MDY`.
///|
/// documented: Oracle mode formats date text from `nls_date_format`, which
/// this instance does not expose.
pub fn Dialect::timestamp_text_example(self : Dialect) -> String {
match self.mode {
Pg => "2022-01-01 10:20:30"
Oracle => "2022-01-01 10:20:30"
Mysql => "2022-01-01 10:20:30"
Sqlserver => "2022-01-01 10:20:30.000"
}
}
///| Literal text for a boolean value on `COPY`.
///|
/// measured: a `bit` column accepts the text `1`; a boolean cast here prints
/// `t`. The loader sends `1` and `0`, which both spellings take.
///|
/// The text a boolean column takes on input, and the text it prints.
///|
/// This follows the type the mode chooses, not the mode name. measured: with the
/// `bit` column sqlserver mode picks, the value prints as `1`; with `number(1)`
/// oracle mode prints `1`; with a `boolean` column both pg mode and mysql mode
/// print `t`, which surprised us in mysql mode, where MySQL's own boolean is a
/// `tinyint(1)` that prints `1`. KingbaseES keeps the PostgreSQL boolean there,
/// and its text form is `t`/`f`.
///|
/// Writing `1` into a PostgreSQL boolean is accepted as true, so an old loader
/// that sent numeric text still worked; reading it back did not match the text it
/// had sent. Spelling the value the way the column spells it is what makes a
/// round trip check possible.
pub fn Dialect::boolean_text(self : Dialect, value : Bool) -> String {
if self.column_type(Boolean) == "boolean" {
if value {
"t"
} else {
"f"
}
} else if value {
"1"
} else {
"0"
}
}
///| Query that reads one setting, in a form all four modes take.
///|
/// measured: `select current_setting('database_mode')` and
/// `show database_mode` both work here.
pub fn Dialect::setting_query(key : String) -> String {
"select current_setting('\{key}')"
}
///|
/// `sum` over an integer column, widened so the total cannot wrap.
///|
/// measured on one instance per mode, over the same 1,000,000 deterministic rows:
/// sqlserver mode answers `pg_typeof(sum(cust_id)) = int` and reports
/// 1335346292, while pg and mysql mode answer `bigint` and oracle mode `numeric`,
/// all three reporting 9925280884. The two numbers differ by exactly 2^33, so
/// sqlserver mode wrapped the total silently instead of raising. Casting the
/// column first gives 9925280884 in every mode.
///|
/// An aggregate checksum taken in one mode therefore does not equal the same
/// checksum taken in another, which matters when a benchmark result is compared
/// across instances.
pub fn Dialect::wide_sum(self : Dialect, column : String) -> String {
match self.mode {
Sqlserver => "sum(cast(\{column} as bigint))"
Pg | Oracle | Mysql => "sum(\{column})"
}
}
///| Query that lists the indexes of one table.
///|
/// measured: `sys_indexes` returns the five index names of the benchmark table in
/// sqlserver, oracle and mysql mode; pg mode has no `sys_` view and answers
/// `pg_indexes` instead, so the view name comes from `catalog_view`.
pub fn Dialect::index_list_query(self : Dialect, table : String) -> String {
let view = self.catalog_view("indexes")
"select indexname from \{view} where tablename=\{sql_literal(table)} order by 1"
}
///| One value as a SQL string literal.
///|
/// measured: a value compared with a catalog column is written in single quotes
/// in every mode of this instance, while an identifier is written in double
/// quotes or brackets. Quoting the wrong kind is a 42703 "column does not
/// exist", not a type error, so the two must stay separate calls.
///|
/// An apostrophe inside the value is doubled, which is the standard escape and
/// needs no backslash, so the literal is safe whatever `standard_conforming_strings`
/// says.
pub fn sql_literal(value : String) -> String {
let q = quote_code(39)
q + value.replace(old=q, new=q + q) + q
}