pgx_sql_entity_graph/pg_extern/entity/
mod.rs

1/*
2Portions Copyright 2019-2021 ZomboDB, LLC.
3Portions Copyright 2021-2022 Technology Concepts & Design, Inc. <support@tcdi.com>
4
5All rights reserved.
6
7Use of this source code is governed by the MIT license that can be found in the LICENSE file.
8*/
9/*!
10
11`#[pg_extern]` related entities for Rust to SQL translation
12
13> Like all of the [`sql_entity_graph`][crate::pgx_sql_entity_graph] APIs, this is considered **internal**
14to the `pgx` framework and very subject to change between versions. While you may use this, please do it with caution.
15
16*/
17mod argument;
18mod operator;
19mod returning;
20
21pub use argument::PgExternArgumentEntity;
22pub use operator::PgOperatorEntity;
23pub use returning::{PgExternReturnEntity, PgExternReturnEntityIteratedItem};
24
25use crate::metadata::{Returns, SqlMapping};
26use crate::pgx_sql::PgxSql;
27use crate::to_sql::entity::ToSqlConfigEntity;
28use crate::to_sql::ToSql;
29use crate::ExternArgs;
30use crate::{SqlGraphEntity, SqlGraphIdentifier};
31
32use eyre::{eyre, WrapErr};
33
34/// The output of a [`PgExtern`](crate::pg_extern::PgExtern) from `quote::ToTokens::to_tokens`.
35#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
36pub struct PgExternEntity {
37    pub name: &'static str,
38    pub unaliased_name: &'static str,
39    pub module_path: &'static str,
40    pub full_path: &'static str,
41    pub metadata: crate::metadata::FunctionMetadataEntity,
42    pub fn_args: Vec<PgExternArgumentEntity>,
43    pub fn_return: PgExternReturnEntity,
44    pub schema: Option<&'static str>,
45    pub file: &'static str,
46    pub line: u32,
47    pub extern_attrs: Vec<ExternArgs>,
48    pub search_path: Option<Vec<&'static str>>,
49    pub operator: Option<PgOperatorEntity>,
50    pub to_sql_config: ToSqlConfigEntity,
51}
52
53impl From<PgExternEntity> for SqlGraphEntity {
54    fn from(val: PgExternEntity) -> Self {
55        SqlGraphEntity::Function(val)
56    }
57}
58
59impl SqlGraphIdentifier for PgExternEntity {
60    fn dot_identifier(&self) -> String {
61        format!("fn {}", self.name)
62    }
63    fn rust_identifier(&self) -> String {
64        self.full_path.to_string()
65    }
66
67    fn file(&self) -> Option<&'static str> {
68        Some(self.file)
69    }
70
71    fn line(&self) -> Option<u32> {
72        Some(self.line)
73    }
74}
75
76impl ToSql for PgExternEntity {
77    fn to_sql(&self, context: &PgxSql) -> eyre::Result<String> {
78        let self_index = context.externs[self];
79        let mut extern_attrs = self.extern_attrs.clone();
80        // if we already have a STRICT marker we do not need to add it
81        // presume we can upgrade, then disprove it
82        let mut strict_upgrade = !extern_attrs.iter().any(|i| i == &ExternArgs::Strict);
83        if strict_upgrade {
84            // It may be possible to infer a `STRICT` marker though.
85            // But we can only do that if the user hasn't used `Option<T>` or `pgx::Internal`
86            for arg in &self.metadata.arguments {
87                if arg.optional {
88                    strict_upgrade = false;
89                }
90            }
91        }
92
93        if strict_upgrade {
94            extern_attrs.push(ExternArgs::Strict);
95        }
96        extern_attrs.sort();
97        extern_attrs.dedup();
98
99        let module_pathname = &context.get_module_pathname();
100
101        let fn_sql = format!(
102            "\
103                CREATE {or_replace} FUNCTION {schema}\"{name}\"({arguments}) {returns}\n\
104                {extern_attrs}\
105                {search_path}\
106                LANGUAGE c /* Rust */\n\
107                AS '{module_pathname}', '{unaliased_name}_wrapper';\
108            ",
109            or_replace =
110                if extern_attrs.contains(&ExternArgs::CreateOrReplace) { "OR REPLACE" } else { "" },
111            schema = self
112                .schema
113                .map(|schema| format!("{}.", schema))
114                .unwrap_or_else(|| context.schema_prefix_for(&self_index)),
115            name = self.name,
116            module_pathname = module_pathname,
117            arguments = if !self.fn_args.is_empty() {
118                let mut args = Vec::new();
119                let metadata_without_arg_skips = &self
120                    .metadata
121                    .arguments
122                    .iter()
123                    .filter(|v| v.argument_sql != Ok(SqlMapping::Skip))
124                    .collect::<Vec<_>>();
125                for (idx, arg) in self.fn_args.iter().enumerate() {
126                    let graph_index = context
127                        .graph
128                        .neighbors_undirected(self_index)
129                        .find(|neighbor| match &context.graph[*neighbor] {
130                            SqlGraphEntity::Type(ty) => ty.id_matches(&arg.used_ty.ty_id),
131                            SqlGraphEntity::Enum(en) => en.id_matches(&arg.used_ty.ty_id),
132                            SqlGraphEntity::BuiltinType(defined) => {
133                                defined == arg.used_ty.full_path
134                            }
135                            _ => false,
136                        })
137                        .ok_or_else(|| eyre!("Could not find arg type in graph. Got: {:?}", arg))?;
138                    let needs_comma = idx < (metadata_without_arg_skips.len().saturating_sub(1));
139                    let metadata_argument = &self.metadata.arguments[idx];
140                    match metadata_argument.argument_sql {
141                        Ok(SqlMapping::As(ref argument_sql)) => {
142                            let buf = format!("\
143                                                \t\"{pattern}\" {variadic}{schema_prefix}{sql_type}{default}{maybe_comma}/* {type_name} */\
144                                            ",
145                                                pattern = arg.pattern,
146                                                schema_prefix = context.schema_prefix_for(&graph_index),
147                                                // First try to match on [`TypeId`] since it's most reliable.
148                                                sql_type = argument_sql,
149                                                default = if let Some(def) = arg.used_ty.default { format!(" DEFAULT {}", def) } else { String::from("") },
150                                                variadic = if metadata_argument.variadic { "VARIADIC " } else { "" },
151                                                maybe_comma = if needs_comma { ", " } else { " " },
152                                                type_name = metadata_argument.type_name,
153                                        );
154                            args.push(buf);
155                        }
156                        Ok(SqlMapping::Composite { array_brackets }) => {
157                            let sql =
158                                self.fn_args[idx]
159                                    .used_ty
160                                    .composite_type
161                                    .map(|v| {
162                                        if array_brackets {
163                                            format!("{v}[]")
164                                        } else {
165                                            format!("{v}")
166                                        }
167                                    })
168                                    .ok_or_else(|| {
169                                        eyre!(
170                                    "Macro expansion time suggested a composite_type!() in return"
171                                )
172                                    })?;
173                            let buf = format!("\
174                                \t\"{pattern}\" {variadic}{schema_prefix}{sql_type}{default}{maybe_comma}/* {type_name} */\
175                            ",
176                                pattern = arg.pattern,
177                                schema_prefix = context.schema_prefix_for(&graph_index),
178                                // First try to match on [`TypeId`] since it's most reliable.
179                                sql_type = sql,
180                                default = if let Some(def) = arg.used_ty.default { format!(" DEFAULT {}", def) } else { String::from("") },
181                                variadic = if metadata_argument.variadic { "VARIADIC " } else { "" },
182                                maybe_comma = if needs_comma { ", " } else { " " },
183                                type_name = metadata_argument.type_name,
184                        );
185                            args.push(buf);
186                        }
187                        Ok(SqlMapping::Source { array_brackets }) => {
188                            let sql =
189                                context
190                                    .source_only_to_sql_type(arg.used_ty.ty_source)
191                                    .map(|v| {
192                                        if array_brackets {
193                                            format!("{v}[]")
194                                        } else {
195                                            format!("{v}")
196                                        }
197                                    })
198                                    .ok_or_else(|| {
199                                        eyre!(
200                                    "Macro expansion time suggested a source only mapping in return"
201                                )
202                                    })?;
203                            let buf = format!("\
204                                \t\"{pattern}\" {variadic}{schema_prefix}{sql_type}{default}{maybe_comma}/* {type_name} */\
205                            ",
206                                pattern = arg.pattern,
207                                schema_prefix = context.schema_prefix_for(&graph_index),
208                                // First try to match on [`TypeId`] since it's most reliable.
209                                sql_type = sql,
210                                default = if let Some(def) = arg.used_ty.default { format!(" DEFAULT {}", def) } else { String::from("") },
211                                variadic = if metadata_argument.variadic { "VARIADIC " } else { "" },
212                                maybe_comma = if needs_comma { ", " } else { " " },
213                                type_name = metadata_argument.type_name,
214                        );
215                            args.push(buf);
216                        }
217                        Ok(SqlMapping::Skip) => (),
218                        Err(err) => {
219                            match context.source_only_to_sql_type(arg.used_ty.ty_source) {
220                                Some(source_only_mapping) => {
221                                    let buf = format!("\
222                                            \t\"{pattern}\" {variadic}{schema_prefix}{sql_type}{default}{maybe_comma}/* {type_name} */\
223                                        ",
224                                            pattern = arg.pattern,
225                                            schema_prefix = context.schema_prefix_for(&graph_index),
226                                            // First try to match on [`TypeId`] since it's most reliable.
227                                            sql_type = source_only_mapping,
228                                            default = if let Some(def) = arg.used_ty.default { format!(" DEFAULT {}", def) } else { String::from("") },
229                                            variadic = if metadata_argument.variadic { "VARIADIC " } else { "" },
230                                            maybe_comma = if needs_comma { ", " } else { " " },
231                                            type_name = metadata_argument.type_name,
232                                    );
233                                    args.push(buf);
234                                }
235                                None => return Err(err).wrap_err("While mapping argument"),
236                            }
237                        }
238                    }
239                }
240                String::from("\n") + &args.join("\n") + "\n"
241            } else {
242                Default::default()
243            },
244            returns = match &self.fn_return {
245                PgExternReturnEntity::None => String::from("RETURNS void"),
246                PgExternReturnEntity::Type { ty } => {
247                    let graph_index = context
248                        .graph
249                        .neighbors_undirected(self_index)
250                        .find(|neighbor| match &context.graph[*neighbor] {
251                            SqlGraphEntity::Type(neighbor_ty) => neighbor_ty.id_matches(&ty.ty_id),
252                            SqlGraphEntity::Enum(neighbor_en) => neighbor_en.id_matches(&ty.ty_id),
253                            SqlGraphEntity::BuiltinType(defined) => &*defined == ty.full_path,
254                            _ => false,
255                        })
256                        .ok_or_else(|| eyre!("Could not find return type in graph."))?;
257                    let metadata_retval = self.metadata.retval.clone().ok_or_else(|| eyre!("Macro expansion time and SQL resolution time had differing opinions about the return value existing"))?;
258                    let metadata_retval_sql = match metadata_retval.return_sql {
259                        Ok(Returns::One(SqlMapping::As(ref sql))) => sql.clone(),
260                        Ok(Returns::One(SqlMapping::Composite { array_brackets })) => ty.composite_type.unwrap().to_string()
261                        + if array_brackets {
262                            "[]"
263                        } else {
264                            ""
265                        },
266                        Ok(Returns::SetOf(SqlMapping::Source { array_brackets })) =>
267                            context.source_only_to_sql_type(ty.ty_source).unwrap().to_string() + if array_brackets {
268                                "[]"
269                            } else {
270                                ""
271                            },
272                        Ok(other) => return Err(eyre!("Got non-plain mapped/composite return variant SQL in what macro-expansion thought was a type, got: {other:?}")),
273                        Err(err) => {
274                            match context.source_only_to_sql_type(ty.ty_source) {
275                                Some(source_only_mapping) => source_only_mapping,
276                                None => return Err(err).wrap_err("Error mapping return SQL")
277                            }
278                        },
279                    };
280                    format!(
281                        "RETURNS {schema_prefix}{sql_type} /* {full_path} */",
282                        sql_type = metadata_retval_sql,
283                        schema_prefix = context.schema_prefix_for(&graph_index),
284                        full_path = ty.full_path
285                    )
286                }
287                PgExternReturnEntity::SetOf { ty, optional: _, result: _ } => {
288                    let graph_index = context
289                        .graph
290                        .neighbors_undirected(self_index)
291                        .find(|neighbor| match &context.graph[*neighbor] {
292                            SqlGraphEntity::Type(neighbor_ty) => neighbor_ty.id_matches(&ty.ty_id),
293                            SqlGraphEntity::Enum(neighbor_en) => neighbor_en.id_matches(&ty.ty_id),
294                            SqlGraphEntity::BuiltinType(defined) => defined == ty.full_path,
295                            _ => false,
296                        })
297                        .ok_or_else(|| eyre!("Could not find return type in graph."))?;
298                    let metadata_retval = self.metadata.retval.clone().ok_or_else(|| eyre!("Macro expansion time and SQL resolution time had differing opinions about the return value existing"))?;
299                    let metadata_retval_sql = match metadata_retval.return_sql {
300                            Ok(Returns::SetOf(SqlMapping::As(ref sql))) => sql.clone(),
301                            Ok(Returns::SetOf(SqlMapping::Composite { array_brackets })) =>
302                                ty.composite_type.unwrap().to_string() + if array_brackets {
303                                    "[]"
304                                } else {
305                                    ""
306                                },
307                            Ok(Returns::SetOf(SqlMapping::Source { array_brackets })) =>
308                                context.source_only_to_sql_type(ty.ty_source).unwrap().to_string() + if array_brackets {
309                                    "[]"
310                                } else {
311                                    ""
312                                },
313                            Ok(_other) => return Err(eyre!("Got non-setof mapped/composite return variant SQL in what macro-expansion thought was a setof")),
314                            Err(err) => return Err(err).wrap_err("Error mapping return SQL"),
315                        };
316                    format!(
317                        "RETURNS SETOF {schema_prefix}{sql_type} /* {full_path} */",
318                        sql_type = metadata_retval_sql,
319                        schema_prefix = context.schema_prefix_for(&graph_index),
320                        full_path = ty.full_path
321                    )
322                }
323                PgExternReturnEntity::Iterated { tys: table_items, optional: _, result: _ } => {
324                    let mut items = String::new();
325                    let metadata_retval = self.metadata.retval.clone().ok_or_else(|| eyre!("Macro expansion time and SQL resolution time had differing opinions about the return value existing"))?;
326                    let metadata_retval_sqls = match metadata_retval.return_sql {
327                            Ok(Returns::Table(variants)) => {
328                                let mut retval_sqls = vec![];
329                                for (idx, variant) in variants.iter().enumerate() {
330                                    let sql = match variant {
331                                        SqlMapping::As(sql) => sql.clone(),
332                                        SqlMapping::Composite { array_brackets } => {
333                                            let composite = table_items[idx].ty.composite_type.unwrap().to_string();
334                                            composite  + if *array_brackets {
335                                                "[]"
336                                            } else {
337                                                ""
338                                            }
339                                        },
340                                        SqlMapping::Source { array_brackets } =>
341                                            context.source_only_to_sql_type(table_items[idx].ty.ty_source).unwrap() + if *array_brackets {
342                                                "[]"
343                                            } else {
344                                                ""
345                                            },
346                                        SqlMapping::Skip => todo!(),
347                                    };
348                                    retval_sqls.push(sql)
349                                }
350                                retval_sqls
351                            },
352                            Ok(_other) => return Err(eyre!("Got non-table return variant SQL in what macro-expansion thought was a table")),
353                            Err(err) => return Err(err).wrap_err("Error mapping return SQL"),
354                        };
355
356                    for (idx, returning::PgExternReturnEntityIteratedItem { ty, name: col_name }) in
357                        table_items.iter().enumerate()
358                    {
359                        let graph_index =
360                            context.graph.neighbors_undirected(self_index).find(|neighbor| {
361                                match &context.graph[*neighbor] {
362                                    SqlGraphEntity::Type(neightbor_ty) => {
363                                        neightbor_ty.id_matches(&ty.ty_id)
364                                    }
365                                    SqlGraphEntity::Enum(neightbor_en) => {
366                                        neightbor_en.id_matches(&ty.ty_id)
367                                    }
368                                    SqlGraphEntity::BuiltinType(defined) => defined == ty.ty_source,
369                                    _ => false,
370                                }
371                            });
372
373                        let needs_comma = idx < (table_items.len() - 1);
374                        let item = format!(
375                                "\n\t{col_name} {schema_prefix}{ty_resolved}{needs_comma} /* {ty_name} */",
376                                col_name = col_name.expect("An iterator of tuples should have `named!()` macro declarations."),
377                                schema_prefix = if let Some(graph_index) = graph_index {
378                                    context.schema_prefix_for(&graph_index)
379                                } else { "".into() },
380                                ty_resolved = metadata_retval_sqls[idx],
381                                needs_comma = if needs_comma { ", " } else { " " },
382                                ty_name = ty.full_path
383                        );
384                        items.push_str(&item);
385                    }
386                    format!("RETURNS TABLE ({}\n)", items)
387                }
388                PgExternReturnEntity::Trigger => String::from("RETURNS trigger"),
389            },
390            search_path = if let Some(search_path) = &self.search_path {
391                let retval = format!("SET search_path TO {}", search_path.join(", "));
392                retval + "\n"
393            } else {
394                Default::default()
395            },
396            extern_attrs = if extern_attrs.is_empty() {
397                String::default()
398            } else {
399                let mut retval = extern_attrs
400                    .iter()
401                    .filter(|attr| **attr != ExternArgs::CreateOrReplace)
402                    .map(|attr| format!("{}", attr).to_uppercase())
403                    .collect::<Vec<_>>()
404                    .join(" ");
405                retval.push('\n');
406                retval
407            },
408            unaliased_name = self.unaliased_name,
409        );
410
411        let ext_sql = format!(
412            "\n\
413                                -- {file}:{line}\n\
414                                -- {module_path}::{name}\n\
415                                {requires}\
416                                {fn_sql}\
417                            ",
418            name = self.name,
419            module_path = self.module_path,
420            file = self.file,
421            line = self.line,
422            fn_sql = fn_sql,
423            requires = {
424                let requires_attrs = self
425                    .extern_attrs
426                    .iter()
427                    .filter_map(|x| match x {
428                        ExternArgs::Requires(requirements) => Some(requirements),
429                        _ => None,
430                    })
431                    .flatten()
432                    .collect::<Vec<_>>();
433                if !requires_attrs.is_empty() {
434                    format!(
435                        "-- requires:\n{}\n",
436                        requires_attrs
437                            .iter()
438                            .map(|i| format!("--   {}", i))
439                            .collect::<Vec<_>>()
440                            .join("\n")
441                    )
442                } else {
443                    "".to_string()
444                }
445            },
446        );
447
448        let rendered = if let Some(op) = &self.operator {
449            let mut optionals = vec![];
450            if let Some(it) = op.commutator {
451                optionals.push(format!("\tCOMMUTATOR = {}", it));
452            };
453            if let Some(it) = op.negator {
454                optionals.push(format!("\tNEGATOR = {}", it));
455            };
456            if let Some(it) = op.restrict {
457                optionals.push(format!("\tRESTRICT = {}", it));
458            };
459            if let Some(it) = op.join {
460                optionals.push(format!("\tJOIN = {}", it));
461            };
462            if op.hashes {
463                optionals.push(String::from("\tHASHES"));
464            };
465            if op.merges {
466                optionals.push(String::from("\tMERGES"));
467            };
468
469            let left_arg =
470                self.metadata.arguments.get(0).ok_or_else(|| {
471                    eyre!("Did not find `left_arg` for operator `{}`.", self.name)
472                })?;
473            let left_fn_arg = self
474                .fn_args
475                .get(0)
476                .ok_or_else(|| eyre!("Did not find `left_arg` for operator `{}`.", self.name))?;
477            let left_arg_graph_index = context
478                .graph
479                .neighbors_undirected(self_index)
480                .find(|neighbor| match &context.graph[*neighbor] {
481                    SqlGraphEntity::Type(ty) => ty.id_matches(&left_fn_arg.used_ty.ty_id),
482                    SqlGraphEntity::Enum(en) => en.id_matches(&left_fn_arg.used_ty.ty_id),
483                    SqlGraphEntity::BuiltinType(defined) => defined == &left_arg.type_name,
484                    _ => false,
485                })
486                .ok_or_else(|| {
487                    eyre!("Could not find left arg type in graph. Got: {:?}", left_arg)
488                })?;
489            let left_arg_sql = match left_arg.argument_sql {
490                Ok(SqlMapping::As(ref sql)) => sql.clone(),
491                Ok(SqlMapping::Composite { array_brackets }) => {
492                    if array_brackets {
493                        let composite_type = self.fn_args[0].used_ty.composite_type
494                            .ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgx::composite_type!()`"))?;
495                        format!("{composite_type}[]")
496                    } else {
497                        self.fn_args[0].used_ty.composite_type
498                            .ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgx::composite_type!()`"))?.to_string()
499                    }
500                }
501                Ok(SqlMapping::Source { array_brackets }) => {
502                    if array_brackets {
503                        let composite_type = context
504                            .source_only_to_sql_type(self.fn_args[0].used_ty.ty_source)
505                            .ok_or(eyre!(
506                                "Found a source only mapping but no source mapping exists for this"
507                            ))?;
508                        format!("{composite_type}[]")
509                    } else {
510                        context.source_only_to_sql_type(self.fn_args[0].used_ty.ty_source)
511                        .ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgx::composite_type!()`"))?.to_string()
512                    }
513                }
514                Ok(SqlMapping::Skip) => {
515                    return Err(eyre!(
516                        "Found an skipped SQL type in an operator, this is not valid"
517                    ))
518                }
519                Err(err) => return Err(err.into()),
520            };
521
522            let right_arg =
523                self.metadata.arguments.get(1).ok_or_else(|| {
524                    eyre!("Did not find `left_arg` for operator `{}`.", self.name)
525                })?;
526            let right_fn_arg = self
527                .fn_args
528                .get(1)
529                .ok_or_else(|| eyre!("Did not find `left_arg` for operator `{}`.", self.name))?;
530            let right_arg_graph_index = context
531                .graph
532                .neighbors_undirected(self_index)
533                .find(|neighbor| match &context.graph[*neighbor] {
534                    SqlGraphEntity::Type(ty) => ty.id_matches(&right_fn_arg.used_ty.ty_id),
535                    SqlGraphEntity::Enum(en) => en.id_matches(&right_fn_arg.used_ty.ty_id),
536                    SqlGraphEntity::BuiltinType(defined) => defined == &right_arg.type_name,
537                    _ => false,
538                })
539                .ok_or_else(|| {
540                    eyre!("Could not find right arg type in graph. Got: {:?}", right_arg)
541                })?;
542            let right_arg_sql = match right_arg.argument_sql {
543                Ok(SqlMapping::As(ref sql)) => sql.clone(),
544                Ok(SqlMapping::Composite { array_brackets }) => {
545                    if array_brackets {
546                        let composite_type = self.fn_args[1].used_ty.composite_type
547                            .ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgx::composite_type!()`"))?;
548                        format!("{composite_type}[]")
549                    } else {
550                        self.fn_args[0].used_ty.composite_type
551                            .ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgx::composite_type!()`"))?.to_string()
552                    }
553                }
554                Ok(SqlMapping::Source { array_brackets }) => {
555                    if array_brackets {
556                        let composite_type = context
557                            .source_only_to_sql_type(self.fn_args[1].used_ty.ty_source)
558                            .ok_or(eyre!(
559                                "Found a source only mapping but no source mapping exists for this"
560                            ))?;
561                        format!("{composite_type}[]")
562                    } else {
563                        context.source_only_to_sql_type(self.fn_args[1].used_ty.ty_source)
564                        .ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgx::composite_type!()`"))?.to_string()
565                    }
566                }
567                Ok(SqlMapping::Skip) => {
568                    return Err(eyre!(
569                        "Found an skipped SQL type in an operator, this is not valid"
570                    ))
571                }
572                Err(err) => return Err(err.into()),
573            };
574
575            let operator_sql = format!("\n\n\
576                                                    -- {file}:{line}\n\
577                                                    -- {module_path}::{name}\n\
578                                                    CREATE OPERATOR {opname} (\n\
579                                                        \tPROCEDURE=\"{name}\",\n\
580                                                        \tLEFTARG={schema_prefix_left}{left_arg}, /* {left_name} */\n\
581                                                        \tRIGHTARG={schema_prefix_right}{right_arg}{maybe_comma} /* {right_name} */\n\
582                                                        {optionals}\
583                                                    );\
584                                                    ",
585                                                    opname = op.opname.unwrap(),
586                                                    file = self.file,
587                                                    line = self.line,
588                                                    name = self.name,
589                                                    module_path = self.module_path,
590                                                    left_name = left_arg.type_name,
591                                                    right_name = right_arg.type_name,
592                                                    schema_prefix_left = context.schema_prefix_for(&left_arg_graph_index),
593                                                    left_arg = left_arg_sql,
594                                                    schema_prefix_right = context.schema_prefix_for(&right_arg_graph_index),
595                                                    right_arg = right_arg_sql,
596                                                    maybe_comma = if optionals.len() >= 1 { "," } else { "" },
597                                                    optionals = if !optionals.is_empty() { optionals.join(",\n") + "\n" } else { "".to_string() },
598                                            );
599            ext_sql + &operator_sql
600        } else {
601            ext_sql
602        };
603        Ok(rendered)
604    }
605}