Skip to main content

fraiseql_core/schema/compiled/
schema_domain.rs

1//! Domain-specific methods for [`CompiledSchema`].
2//!
3//! Fact table management, observers, federation metadata, security configuration,
4//! RLS, role scopes, tenancy, SDL generation, and schema validation.
5
6#[cfg(feature = "federation")]
7use std::collections::HashMap;
8use std::fmt::Write as _;
9
10use super::schema::{CURRENT_SCHEMA_FORMAT_VERSION, CompiledSchema};
11use crate::{
12    compiler::fact_table::FactTableMetadata,
13    schema::{
14        observer_types::ObserverDefinition,
15        security_config::{RoleDefinition, SecurityConfig},
16    },
17};
18
19impl CompiledSchema {
20    /// Verify that the compiled schema was produced by a compatible compiler version.
21    ///
22    /// Schemas without a `schema_format_version` field (produced before v2.1) are
23    /// accepted with a warning. Schemas with a mismatched version are rejected to
24    /// prevent silent data corruption from structural changes.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error string if the version is present and incompatible.
29    pub fn validate_format_version(&self) -> Result<(), String> {
30        match self.schema_format_version {
31            None => {
32                // Pre-versioning schema — accept but callers may want to warn.
33                Ok(())
34            },
35            Some(v) if v == CURRENT_SCHEMA_FORMAT_VERSION => Ok(()),
36            Some(v) => Err(format!(
37                "Schema format version mismatch: compiled schema has version {v}, \
38                 but this runtime expects version {CURRENT_SCHEMA_FORMAT_VERSION}. \
39                 Please recompile your schema with the matching fraiseql-cli version."
40            )),
41        }
42    }
43
44    /// Register fact table metadata.
45    ///
46    /// # Arguments
47    ///
48    /// * `table_name` - Fact table name (e.g., `tf_sales`)
49    /// * `metadata` - Typed `FactTableMetadata`
50    pub fn add_fact_table(&mut self, table_name: String, metadata: FactTableMetadata) {
51        self.fact_tables.insert(table_name, metadata);
52    }
53
54    /// Get fact table metadata by name.
55    ///
56    /// # Arguments
57    ///
58    /// * `name` - Fact table name
59    ///
60    /// # Returns
61    ///
62    /// Fact table metadata if found
63    #[must_use]
64    pub fn get_fact_table(&self, name: &str) -> Option<&FactTableMetadata> {
65        self.fact_tables.get(name)
66    }
67
68    /// List all fact table names.
69    ///
70    /// # Returns
71    ///
72    /// Vector of fact table names
73    #[must_use]
74    pub fn list_fact_tables(&self) -> Vec<&str> {
75        self.fact_tables.keys().map(String::as_str).collect()
76    }
77
78    /// Check if schema contains any fact tables.
79    #[must_use]
80    pub fn has_fact_tables(&self) -> bool {
81        !self.fact_tables.is_empty()
82    }
83
84    /// Find an observer definition by name.
85    #[must_use]
86    pub fn find_observer(&self, name: &str) -> Option<&ObserverDefinition> {
87        self.observers.iter().find(|o| o.name == name)
88    }
89
90    /// Get all observers for a specific entity type.
91    #[must_use]
92    pub fn find_observers_for_entity(&self, entity: &str) -> Vec<&ObserverDefinition> {
93        self.observers.iter().filter(|o| o.entity == entity).collect()
94    }
95
96    /// Get all observers for a specific event type (INSERT, UPDATE, DELETE).
97    #[must_use]
98    pub fn find_observers_for_event(&self, event: &str) -> Vec<&ObserverDefinition> {
99        self.observers.iter().filter(|o| o.event == event).collect()
100    }
101
102    /// Check if schema contains any observers.
103    #[must_use]
104    pub const fn has_observers(&self) -> bool {
105        !self.observers.is_empty()
106    }
107
108    /// Get total number of observers.
109    #[must_use]
110    pub const fn observer_count(&self) -> usize {
111        self.observers.len()
112    }
113
114    /// Get federation metadata from schema.
115    ///
116    /// # Returns
117    ///
118    /// Federation metadata if configured in schema
119    #[cfg(feature = "federation")]
120    #[must_use]
121    pub fn federation_metadata(&self) -> Option<crate::federation::FederationMetadata> {
122        self.federation.as_ref().filter(|fed| fed.enabled).map(|fed| {
123            use crate::federation::types::{
124                FederatedType, FieldFederationDirectives, KeyDirective,
125            };
126
127            // Entities carry an `@key` (and, for an extended entity, `extend type` +
128            // `@external` on the borrowed key/fields). Per-field directives are
129            // rebuilt from the entity's `external_fields` / `shareable_fields` so the
130            // SDL renderer can append `@external` / `@shareable` to each field line.
131            let mut types: Vec<FederatedType> = fed
132                .entities
133                .iter()
134                .map(|e| {
135                    let mut field_directives: HashMap<String, FieldFederationDirectives> =
136                        HashMap::new();
137                    for f in &e.external_fields {
138                        field_directives.entry(f.clone()).or_default().external = true;
139                    }
140                    for f in &e.shareable_fields {
141                        field_directives.entry(f.clone()).or_default().shareable = true;
142                    }
143                    FederatedType {
144                        name: e.name.clone(),
145                        keys: vec![KeyDirective {
146                            fields:     e.key_fields.clone(),
147                            resolvable: true,
148                        }],
149                        is_extends: e.extends,
150                        external_fields: e.external_fields.clone(),
151                        shareable_fields: e.shareable_fields.clone(),
152                        inaccessible_fields: Vec::new(),
153                        field_directives,
154                        type_shareable: false,
155                    }
156                })
157                .collect();
158
159            // Non-entity `@shareable` value types (e.g. a shared `MutationError`):
160            // no `@key`, never a member of the `_Entity` union — they only receive a
161            // type-level `@shareable` so both subgraphs can define the identical type
162            // without an `INVALID_FIELD_SHARING` composition error.
163            for name in &fed.shareable_types {
164                types.push(FederatedType {
165                    name:                name.clone(),
166                    keys:                Vec::new(),
167                    is_extends:          false,
168                    external_fields:     Vec::new(),
169                    shareable_fields:    Vec::new(),
170                    inaccessible_fields: Vec::new(),
171                    field_directives:    HashMap::new(),
172                    type_shareable:      true,
173                });
174            }
175
176            crate::federation::FederationMetadata {
177                enabled: fed.enabled,
178                version: fed.version.clone().unwrap_or_else(|| "v2".to_string()),
179                types,
180                remote_subscription_fields: HashMap::new(),
181            }
182        })
183    }
184
185    /// Build the per-entity-type backing source map (`typename` →
186    /// [`EntitySource`](crate::federation::EntitySource)) the federation
187    /// `_entities` resolver reads from instead of guessing `lower(typename)`
188    /// (#504/#507).
189    ///
190    /// Two sources, query-wins:
191    ///
192    /// 1. **Query-sourced** (owned entities): the backing relation rides on the root query that
193    ///    returns the type, keyed by `return_type`, first-wins — the same query→type binding the
194    ///    Relay `node` path uses. The query's `jsonb_column` (which the compiler defaults to
195    ///    `"data"`) drives jsonb projection.
196    /// 2. **Type-sourced fallback** (#507): an owner-split `extend type … @key` entity resolved in
197    ///    a subgraph that does not own it exposes no root query, so there is nothing in (1) to
198    ///    source its relation from. Its relation instead rides on the type-level `sql_source` the
199    ///    compiler carries from the authoring SDK. This only fills gaps — a query-sourced entry
200    ///    always wins.
201    ///
202    /// Both sources read the entity's `jsonb_column` the same way: a non-empty column selects
203    /// jsonb-projection mode (`<col>->'<field>'`), an empty one selects flat-column mode (bare
204    /// columns). The compiler defaults both a query's and an extends type's `jsonb_column` to the
205    /// standard `"data"` view shape, so a flat-column entity must be authored with an explicit
206    /// empty `jsonb_column`.
207    #[cfg(feature = "federation")]
208    #[must_use]
209    pub fn entity_sources(&self) -> HashMap<String, crate::federation::EntitySource> {
210        use crate::federation::EntitySource;
211
212        let mut sources: HashMap<String, EntitySource> = HashMap::new();
213
214        // (1) Query-sourced — owned entities. First-wins per return_type.
215        for q in &self.queries {
216            if let Some(relation) = &q.sql_source {
217                sources.entry(q.return_type.clone()).or_insert_with(|| EntitySource {
218                    relation:     relation.clone(),
219                    jsonb_column: (!q.jsonb_column.is_empty()).then(|| q.jsonb_column.clone()),
220                });
221            }
222        }
223
224        // (2) Type-sourced fallback — owner-split `extend type` entities (#507).
225        // Skipped for owned types (their type-level sql_source is empty) and never
226        // overrides a query-sourced entry. The empty-jsonb-column → flat-mode rule
227        // mirrors the query path above, so flat-column extends entities resolve too.
228        for t in &self.types {
229            if t.sql_source.as_str().is_empty() {
230                continue;
231            }
232            sources.entry(t.name.to_string()).or_insert_with(|| EntitySource {
233                relation:     t.sql_source.to_string(),
234                jsonb_column: (!t.jsonb_column.is_empty()).then(|| t.jsonb_column.clone()),
235            });
236        }
237
238        sources
239    }
240
241    /// Stub federation metadata when federation feature is disabled.
242    #[cfg(not(feature = "federation"))]
243    #[must_use]
244    pub const fn federation_metadata(&self) -> Option<()> {
245        None
246    }
247
248    /// Get security configuration from schema.
249    ///
250    /// # Returns
251    ///
252    /// Security configuration if present (includes role definitions)
253    #[must_use]
254    pub const fn security_config(&self) -> Option<&SecurityConfig> {
255        self.security.as_ref()
256    }
257
258    /// Returns `true` if this schema declares a multi-tenant deployment.
259    ///
260    /// Multi-tenant schemas require Row-Level Security (RLS) to be active whenever
261    /// query result caching is enabled. Without RLS, all tenants sharing the same
262    /// query parameters would receive the same cached response.
263    ///
264    /// Detection is based on `security.multi_tenant` in the compiled schema JSON.
265    #[must_use]
266    pub fn is_multi_tenant(&self) -> bool {
267        self.security.as_ref().is_some_and(|s| s.multi_tenant)
268    }
269
270    /// Returns the tenancy isolation mode configured for this schema.
271    ///
272    /// Defaults to `TenancyMode::None` when no security or tenancy configuration
273    /// is present, meaning single-tenant operation with no isolation machinery.
274    #[must_use]
275    pub fn tenancy_mode(&self) -> crate::schema::TenancyMode {
276        self.security
277            .as_ref()
278            .map_or(crate::schema::TenancyMode::None, |s| s.tenancy.mode)
279    }
280
281    /// Returns the tenancy configuration, if present.
282    ///
283    /// Returns `None` when no security configuration exists. Returns the
284    /// default `TenancyConfig` (mode=none) when security exists but tenancy
285    /// is not explicitly configured.
286    #[must_use]
287    pub fn tenancy_config(&self) -> Option<&crate::schema::TenancyConfig> {
288        self.security.as_ref().map(|s| &s.tenancy)
289    }
290
291    /// Find a role definition by name.
292    ///
293    /// # Arguments
294    ///
295    /// * `role_name` - Name of the role to find
296    ///
297    /// # Returns
298    ///
299    /// Role definition if found
300    #[must_use]
301    pub fn find_role(&self, role_name: &str) -> Option<RoleDefinition> {
302        self.security.as_ref().and_then(|config| config.find_role(role_name).cloned())
303    }
304
305    /// Get scopes for a role.
306    ///
307    /// # Arguments
308    ///
309    /// * `role_name` - Name of the role
310    ///
311    /// # Returns
312    ///
313    /// Vector of scopes granted to the role
314    #[must_use]
315    pub fn get_role_scopes(&self, role_name: &str) -> Vec<String> {
316        self.security
317            .as_ref()
318            .map(|config| config.get_role_scopes(role_name))
319            .unwrap_or_default()
320    }
321
322    /// Check if a role has a specific scope.
323    ///
324    /// # Arguments
325    ///
326    /// * `role_name` - Name of the role
327    /// * `scope` - Scope to check for
328    ///
329    /// # Returns
330    ///
331    /// true if role has the scope, false otherwise
332    #[must_use]
333    pub fn role_has_scope(&self, role_name: &str, scope: &str) -> bool {
334        self.security
335            .as_ref()
336            .is_some_and(|config| config.role_has_scope(role_name, scope))
337    }
338
339    /// Returns `true` if Row-Level Security policies are declared in this schema.
340    ///
341    /// Used at server startup to validate that caching is safe for multi-tenant
342    /// deployments. When caching is enabled and no RLS policies are configured,
343    /// the server emits a startup warning about potential data leakage.
344    ///
345    /// # Example
346    ///
347    /// ```
348    /// use fraiseql_core::schema::CompiledSchema;
349    ///
350    /// let schema = CompiledSchema::default();
351    /// assert!(!schema.has_rls_configured());
352    /// ```
353    #[must_use]
354    pub fn has_rls_configured(&self) -> bool {
355        self.security.as_ref().is_some_and(|s| {
356            !s.additional
357                .get("policies")
358                .and_then(|p: &serde_json::Value| p.as_array())
359                .is_none_or(|a| a.is_empty())
360        })
361    }
362
363    /// Get raw GraphQL schema SDL.
364    ///
365    /// # Returns
366    ///
367    /// Raw schema string if available, otherwise generates from type definitions,
368    /// including the root `Query`/`Mutation` types.
369    ///
370    /// Root operations are stored in [`self.queries`](Self::queries) /
371    /// [`self.mutations`](Self::mutations) rather than as `Query`/`Mutation` object
372    /// types in [`self.types`](Self::types), so they are rendered here explicitly.
373    /// Omitting them produces an SDL that advertises no root fields — which makes the
374    /// federation `_service` SDL (built from this output) fail gateway composition
375    /// with `NO_QUERIES`.
376    #[must_use]
377    pub fn raw_schema(&self) -> String {
378        self.schema_sdl.clone().unwrap_or_else(|| {
379            // Generate basic SDL from type definitions if not provided
380            let mut sdl = String::new();
381
382            // Non-built-in scalar declarations. The rendered operations and fields
383            // reference custom and standard-but-non-built-in scalars (`DateTime`,
384            // `JSON`, `Decimal`, rich scalars, …); a gateway composing the subgraph
385            // reports `Unknown type` for any it isn't declared.
386            for name in self.referenced_scalars() {
387                let _ = writeln!(sdl, "scalar {name}");
388            }
389            if !self.enums.is_empty()
390                || !self.interfaces.is_empty()
391                || !self.input_types.is_empty()
392                || !self.unions.is_empty()
393                || !self.types.is_empty()
394            {
395                sdl.push('\n');
396            }
397
398            // Enum types
399            for enum_def in &self.enums {
400                let _ = writeln!(sdl, "enum {} {{", enum_def.name);
401                for value in &enum_def.values {
402                    let _ = writeln!(sdl, "  {}", value.name);
403                }
404                sdl.push_str("}\n\n");
405            }
406
407            // Interface types
408            for iface in &self.interfaces {
409                let _ = writeln!(sdl, "interface {} {{", iface.name);
410                for field in &iface.fields {
411                    let _ = writeln!(sdl, "  {}: {}", field.name, field.field_type);
412                }
413                sdl.push_str("}\n\n");
414            }
415
416            // Input object types (`field_type` is a pre-rendered GraphQL string;
417            // normalise the trailing non-null marker against the `nullable` flag).
418            for input in &self.input_types {
419                let _ = writeln!(sdl, "input {} {{", input.name);
420                for field in &input.fields {
421                    let base = field.field_type.trim_end_matches('!');
422                    let non_null = if field.nullable { "" } else { "!" };
423                    let _ = writeln!(sdl, "  {}: {base}{non_null}", field.name);
424                }
425                sdl.push_str("}\n\n");
426            }
427
428            // Union types (covers synthesized mutation result unions)
429            for union_def in &self.unions {
430                let _ = writeln!(
431                    sdl,
432                    "union {} = {}",
433                    union_def.name,
434                    union_def.member_types.join(" | ")
435                );
436            }
437            if !self.unions.is_empty() {
438                sdl.push('\n');
439            }
440
441            // Add output/object types
442            for type_def in &self.types {
443                let _ = writeln!(sdl, "type {} {{", type_def.name);
444                for field in &type_def.fields {
445                    let _ = writeln!(sdl, "  {}: {}", field.name, field.field_type);
446                }
447                sdl.push_str("}\n\n");
448            }
449
450            // Root Query type (rendered from `self.queries`, never present in `types`)
451            if !self.queries.is_empty() {
452                sdl.push_str("type Query {\n");
453                for q in &self.queries {
454                    let _ = writeln!(
455                        sdl,
456                        "  {}",
457                        render_operation_field(
458                            &q.name,
459                            &q.graphql_arguments(),
460                            &q.return_type,
461                            q.returns_list,
462                            q.nullable,
463                        )
464                    );
465                }
466                sdl.push_str("}\n\n");
467            }
468
469            // Root Mutation type (rendered from `self.mutations`). Mutation payloads
470            // are single, non-null values, so they render as `Name(args): Return!`.
471            if !self.mutations.is_empty() {
472                sdl.push_str("type Mutation {\n");
473                for m in &self.mutations {
474                    let _ = writeln!(
475                        sdl,
476                        "  {}",
477                        render_operation_field(&m.name, &m.arguments, &m.return_type, false, false)
478                    );
479                }
480                sdl.push_str("}\n\n");
481            }
482
483            sdl
484        })
485    }
486
487    /// Collect the non-built-in scalar type names the schema references, so
488    /// [`raw_schema`](Self::raw_schema) can declare each one (`scalar Name`) and the
489    /// SDL is type-complete.
490    ///
491    /// A referenced type is treated as a scalar to declare when it is neither a
492    /// built-in GraphQL scalar nor a type the schema defines as an object, enum,
493    /// input, interface, or union. Names are collected **exactly as the fields render
494    /// them** (the verbatim leaf of each field/argument type and each operation return
495    /// type) so the declaration and the reference always agree — declaring a canonical
496    /// alias (`DateTime`) while a field renders `datetime` would leave the reference
497    /// dangling (`Unknown type datetime`). The custom-scalar registry is also included.
498    /// The federation `_Any`/`_Entity`/`_Service`/`_FieldSet` built-ins (supplied by the
499    /// federation layer) are excluded.
500    fn referenced_scalars(&self) -> Vec<String> {
501        use std::collections::{BTreeSet, HashSet};
502
503        const BUILTINS: [&str; 5] = ["String", "Int", "Float", "Boolean", "ID"];
504        const FED_BUILTINS: [&str; 4] = ["_Any", "_Entity", "_Service", "_FieldSet"];
505
506        // Names the schema defines as composite types — never re-declared as scalars.
507        let mut defined: HashSet<&str> = HashSet::new();
508        for t in &self.types {
509            defined.insert(t.name.as_str());
510        }
511        for e in &self.enums {
512            defined.insert(e.name.as_str());
513        }
514        for i in &self.input_types {
515            defined.insert(i.name.as_str());
516        }
517        for i in &self.interfaces {
518            defined.insert(i.name.as_str());
519        }
520        for u in &self.unions {
521            defined.insert(u.name.as_str());
522        }
523
524        // Every type reference, collected as the verbatim leaf name fields render.
525        let mut referenced: BTreeSet<String> = BTreeSet::new();
526        let add = |rendered: &str, set: &mut BTreeSet<String>| {
527            let leaf = leaf_type_name(rendered);
528            if !leaf.is_empty() {
529                set.insert(leaf);
530            }
531        };
532        for type_def in &self.types {
533            for field in &type_def.fields {
534                add(&field.field_type.to_string(), &mut referenced);
535            }
536        }
537        for iface in &self.interfaces {
538            for field in &iface.fields {
539                add(&field.field_type.to_string(), &mut referenced);
540            }
541        }
542        for query in &self.queries {
543            // Walk the *rendered* arguments so any scalar synthesized for an
544            // `auto_params` query (notably `JSON` for `where`/`orderBy`) is
545            // declared — `render_operation_field` renders this same list.
546            for arg in &query.graphql_arguments() {
547                add(&arg.arg_type.to_string(), &mut referenced);
548            }
549            add(&query.return_type, &mut referenced);
550        }
551        for mutation in &self.mutations {
552            for arg in &mutation.arguments {
553                add(&arg.arg_type.to_string(), &mut referenced);
554            }
555            add(&mutation.return_type, &mut referenced);
556        }
557        for input in &self.input_types {
558            for field in &input.fields {
559                add(&field.field_type, &mut referenced);
560            }
561        }
562        for (name, _) in self.custom_scalars.list_all() {
563            referenced.insert(name);
564        }
565
566        referenced
567            .into_iter()
568            .filter(|name| {
569                !defined.contains(name.as_str())
570                    && !BUILTINS.contains(&name.as_str())
571                    && !FED_BUILTINS.contains(&name.as_str())
572            })
573            .collect()
574    }
575
576    /// Validate the schema for internal consistency.
577    ///
578    /// Checks:
579    /// - All type references resolve to defined types
580    /// - No duplicate type/operation names
581    /// - Required fields have valid types
582    ///
583    /// # Errors
584    ///
585    /// Returns list of validation errors if schema is invalid.
586    pub fn validate(&self) -> Result<(), Vec<String>> {
587        let mut errors = Vec::new();
588
589        // Check for duplicate type names
590        let mut type_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
591        for type_def in &self.types {
592            if !type_names.insert(type_def.name.as_str()) {
593                errors.push(format!("Duplicate type name: {}", type_def.name));
594            }
595        }
596
597        // Check for duplicate query names
598        let mut query_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
599        for query in &self.queries {
600            if !query_names.insert(&query.name) {
601                errors.push(format!("Duplicate query name: {}", query.name));
602            }
603        }
604
605        // Check for duplicate mutation names
606        let mut mutation_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
607        for mutation in &self.mutations {
608            if !mutation_names.insert(&mutation.name) {
609                errors.push(format!("Duplicate mutation name: {}", mutation.name));
610            }
611        }
612
613        // Check type references in queries
614        for query in &self.queries {
615            if !type_names.contains(query.return_type.as_str())
616                && !is_builtin_type(&query.return_type)
617            {
618                errors.push(format!(
619                    "Query '{}' references undefined type '{}'",
620                    query.name, query.return_type
621                ));
622            }
623        }
624
625        // Check type references in mutations
626        for mutation in &self.mutations {
627            if !type_names.contains(mutation.return_type.as_str())
628                && !is_builtin_type(&mutation.return_type)
629            {
630                errors.push(format!(
631                    "Mutation '{}' references undefined type '{}'",
632                    mutation.name, mutation.return_type
633                ));
634            }
635        }
636
637        if errors.is_empty() {
638            Ok(())
639        } else {
640            Err(errors)
641        }
642    }
643}
644
645/// Render a root operation as a GraphQL SDL field: `name(arg: T!, …): Return`.
646///
647/// `return_type` is a bare type name; list-ness and nullability are applied here so
648/// the rendered signature matches GraphQL conventions (`[User!]!`, `User`, `User!`).
649fn render_operation_field(
650    name: &str,
651    arguments: &[crate::schema::ArgumentDefinition],
652    return_type: &str,
653    returns_list: bool,
654    nullable: bool,
655) -> String {
656    let non_null = if nullable { "" } else { "!" };
657    let ret = if returns_list {
658        format!("[{return_type}!]{non_null}")
659    } else {
660        format!("{return_type}{non_null}")
661    };
662    if arguments.is_empty() {
663        return format!("{name}: {ret}");
664    }
665    let args = arguments
666        .iter()
667        .map(|a| format!("{}: {}{}", a.name, a.arg_type, if a.nullable { "" } else { "!" }))
668        .collect::<Vec<_>>()
669        .join(", ");
670    format!("{name}({args}): {ret}")
671}
672
673/// Strip GraphQL list and non-null markers from a rendered type string, leaving the
674/// bare leaf type name: `[User!]!` → `User`, `datetime` → `datetime`.
675fn leaf_type_name(rendered: &str) -> String {
676    rendered
677        .chars()
678        .filter(|c| !matches!(c, '[' | ']' | '!'))
679        .collect::<String>()
680        .trim()
681        .to_string()
682}
683
684/// Check if a type name is a built-in scalar type.
685fn is_builtin_type(name: &str) -> bool {
686    matches!(
687        name,
688        "String"
689            | "Int"
690            | "Float"
691            | "Boolean"
692            | "ID"
693            | "DateTime"
694            | "Date"
695            | "Time"
696            | "JSON"
697            | "UUID"
698            | "Decimal"
699    )
700}