Repository: geldata/gel
Pull request: Support creating indexes concurrently
The implementation in this PR works well and is solid, but there is plenty of room for us to argue about whether the interface is good enough.

The core idea, though, is to have an index field called `create_concurrently` that indicates that an index should be created concurrently, and then there will be a separate CLI command used to actually trigger and wait for the concurrent creation.

The crux of the complication here is that CREATE INDEX CONCURRENTLY cannot be run from inside a transaction or a pl/pgsql script, and so it can't be transactionally packaged with anything else, like updating our metadata.

The approach, then, is to have three separate components:
 * The actual DDL, generated by `migration create`, which will put the index
   into our schema but not actually create any indexes.
   The `create_concurrently` flag is exposed, as well as a new `active` flag
   that indicates whether the backing index actually exists.
 * `ADMINISTER concurrent_index_create(<uuid>)`, which takes the `id` of
    an index and runs `CREATE INDEX CONCURRENTLY IF NOT EXISTS` for it,
    and then sets `active := true` on the schema object (and populates postgres
    schema comments). 
    It does *not* do these things transactionally, though, so we could crash or
    shutdown after creating the index but before updating the schema. That's fine,
    though: once the command is run again, it will skip the CREATE INDEX then update
    metadata.

Fixes #8175

------

The big questions here are:
 * Is it acceptable to have create_concurrently as a schema flag?
   I chose that because users have a very schema-driven workflow,
   and the alternative seemed to be to force them to edit the migrations
   to add it.
 * Is outsourcing the state machine processing for actually creating
   the indexes to the CLI invoking ADMINISTER commands reasonable?
   It was quite straightforward to build.


I didn't try to make use of any of the `deferred` mechanism, because
that seems like it solving a different issue.


Problem statement:
Concurrent index creation
Postgres supports `CREATE INDEX CONCURRENTLY`, and it would be good if we can do something similar.

The two big hurdles are:
 - How should it interact with our migration system
 - `CREATE INDEX CONCURRENTLY` does not work in a transaction, and we always do our DDL transactionally now

Hints from issue discussion:

Commit message: lint

Patch to review:
diff --git a/dev-notes/concurrent-indexes.py b/dev-notes/concurrent-indexes.py
new file mode 100644
index 00000000000..02abec1af4b
--- /dev/null
+++ b/dev-notes/concurrent-indexes.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+
+import gel
+
+def create_concurrent_indexes(db, msg_callback=print):
+    '''Actually create all "create concurrently" indexes
+
+    The protocol here is to find all the indexes that need created,
+    and create them with `administer concurrent_index_create()`.
+    It's possible that the database will shut down after an index
+    creation but before the metadata is updated, in which case
+    we might rerun the command later, which is harmless.
+
+    If we stick with this ADMINISTER-based schemed, I figure this code
+    would live in the CLI.
+    '''
+    indexes = db.query('''
+        select schema::Index {
+            id, expr, subject_name := .<indexes[is schema::ObjectType].name
+        }
+        filter .create_concurrently and not .active
+    ''')
+    for index in indexes:
+        msg_callback(
+            f"Creating concurrent index on '{index.subject_name}' "
+            f"with expr ({index.expr})"
+        )
+        db.execute(f'''
+            administer concurrent_index_create("{index.id}")
+        ''')
+
+
+def main():
+    with gel.create_client() as db:
+        create_concurrent_indexes(db)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/edb/buildmeta.py b/edb/buildmeta.py
index 5d98dba8af3..426c9b580b4 100644
--- a/edb/buildmeta.py
+++ b/edb/buildmeta.py
@@ -57,7 +57,7 @@
 # The merge conflict there is a nice reminder that you probably need
 # to write a patch in edb/pgsql/patches.py, and then you should preserve
 # the old value.
-EDGEDB_CATALOG_VERSION = 2025_06_26_00_00
+EDGEDB_CATALOG_VERSION = 2025_06_26_00_01
 EDGEDB_MAJOR_VERSION = 7
 
 
diff --git a/edb/edgeql-parser/edgeql-parser-python/src/normalize.rs b/edb/edgeql-parser/edgeql-parser-python/src/normalize.rs
index 9f580588498..15f69af96a4 100644
--- a/edb/edgeql-parser/edgeql-parser-python/src/normalize.rs
+++ b/edb/edgeql-parser/edgeql-parser-python/src/normalize.rs
@@ -156,7 +156,7 @@ pub fn normalize(text: &str) -> Result<Entry, Error> {
             }
             Kind::Keyword(Keyword(kw))
             if (
-                matches!(kw, "configure"|"create"|"alter"|"drop"|"start"|"analyze")
+                matches!(kw, "administer"|"configure"|"create"|"alter"|"drop"|"start"|"analyze")
                 || (last_was_set && kw == "global")
             ) => {
                 let processed_source = serialize_tokens(&tokens);
diff --git a/edb/lib/schema.edgeql b/edb/lib/schema.edgeql
index d518b12e1b7..6353239390c 100644
--- a/edb/lib/schema.edgeql
+++ b/edb/lib/schema.edgeql
@@ -275,6 +275,8 @@ CREATE TYPE schema::Index
     CREATE PROPERTY except_expr -> std::str;
     CREATE PROPERTY deferrability -> schema::IndexDeferrability;
     CREATE PROPERTY deferred -> std::bool;
+    CREATE PROPERTY active -> std::bool;
+    CREATE PROPERTY create_concurrently -> std::bool;
     CREATE MULTI LINK params EXTENDING schema::ordered -> schema::Parameter {
         ON TARGET DELETE ALLOW;
     };
diff --git a/edb/pgsql/dbops/indexes.py b/edb/pgsql/dbops/indexes.py
index 1090bdadff6..28a83b90c08 100644
--- a/edb/pgsql/dbops/indexes.py
+++ b/edb/pgsql/dbops/indexes.py
@@ -74,7 +74,11 @@ def add_columns(self, columns: Iterable[str | pgast.Star]) -> None:
                 raise NotImplementedError()
             self._columns.add(col)
 
-    def creation_code(self) -> str:
+    def creation_code(
+        self,
+        concurrently: bool = False,
+        conditional: bool = False,
+    ) -> str:
         if self.exprs:
             exprs = self.exprs
         else:
@@ -91,7 +95,13 @@ def creation_code(self) -> str:
         code = 'CREATE'
         if self.unique:
             code += ' UNIQUE'
-        code += f' INDEX {qn(self.name_in_catalog)} ON {qn(*self.table_name)}'
+        code += f' INDEX'
+        if concurrently:
+            code += ' CONCURRENTLY'
+        if conditional:
+            code += ' IF NOT EXISTS'
+
+        code += f' {qn(self.name_in_catalog)} ON {qn(*self.table_name)}'
 
         if using:
             code += f' USING {using}'
@@ -203,17 +213,24 @@ def __init__(
         index: Index,
         *,
         conditional: bool = False,
+        builtin_conditional: bool = False,
+        concurrently: bool = False,
         **kwargs: Any
     ) -> None:
         super().__init__(index, **kwargs)
         self.index = index
+        self.concurrently = concurrently
+        self.builtin_conditional = builtin_conditional
         if conditional:
             self.neg_conditions.add(
                 IndexExists((index.table_name[0], index.name_in_catalog))
             )
 
     def code(self) -> str:
-        return self.index.creation_code()
+        return self.index.creation_code(
+            concurrently=self.concurrently,
+            conditional=self.builtin_conditional,
+        )
 
 
 class DropIndex(ddl.DropObject):
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
index a0047681bcb..6c2379dd93a 100644
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -3427,7 +3427,7 @@ def create_index(
         index: s_indexes.Index,
         schema: s_schema.Schema,
         context: sd.CommandContext,
-    ):
+    ) -> dbops.Command:
         from .compiler import astutils
 
         options = get_index_compile_options(
@@ -3540,7 +3540,12 @@ def create_index(
                 'kwargs': sql_kwarg_exprs,
             }
         )
-        return dbops.CreateIndex(pg_index)
+        concurrently = index.get_create_concurrently(schema)
+        return dbops.CreateIndex(
+            pg_index,
+            concurrently=concurrently,
+            builtin_conditional=concurrently,
+        )
 
     def _create_innards(
         self,
@@ -3554,6 +3559,9 @@ def _create_innards(
             # Don't do anything for abstract indexes
             return schema
 
+        if index.get_create_concurrently(schema):
+            return schema
+
         with errors.ensure_span(self.span):
             self.pgops.add(self.create_index(index, schema, context))
 
diff --git a/edb/schema/indexes.py b/edb/schema/indexes.py
index 5bdd9040872..b2c95ef7edc 100644
--- a/edb/schema/indexes.py
+++ b/edb/schema/indexes.py
@@ -341,6 +341,21 @@ class Index(
         merge_fn=merge_deferred,
     )
 
+    # Whether the index is created and populated in pg. Relevant if
+    # create_concurrently is true?
+    active = so.SchemaField(
+        bool,
+        default=True,
+    )
+
+    # XXX: I am not sure this is what I want to do.
+    create_concurrently = so.SchemaField(
+        bool,
+        default=False,
+        compcoef=0.803,
+        allow_ddl_set=True,
+    )
+
     def __repr__(self) -> str:
         cls = self.__class__
         return '<{}.{} {!r} at 0x{:x}>'.format(
@@ -1070,6 +1085,13 @@ def _cmd_tree_from_ast(
                     span=astnode.span,
                 )
 
+            if cmd.get_attribute_span('create_concurrently'):
+                cmd.set_attribute_value(
+                    'active',
+                    False,
+                    span=astnode.span,
+                )
+
         return cmd
 
     @classmethod
@@ -1673,6 +1695,24 @@ class AlterIndex(
     astnode = [qlast.AlterConcreteIndex, qlast.AlterIndex]
     referenced_astnode = qlast.AlterConcreteIndex
 
+    def validate_object(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        super().validate_object(schema, context)
+
+        vn = self.scls.get_verbosename(schema, with_parent=True)
+        if (
+            not self.scls.get_create_concurrently(schema)
+            and not self.scls.get_active(schema)
+        ):
+            raise errors.SchemaDefinitionError(
+                f'{vn} is not active, so create_concurrently may '
+                f'not be cleared',
+                span=self.span,
+            )
+
     def canonicalize_alter_from_external_ref(
         self,
         schema: s_schema.Schema,
diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py
index 5651e378d40..26f1a575ce1 100644
--- a/edb/server/compiler/compiler.py
+++ b/edb/server/compiler/compiler.py
@@ -1508,6 +1508,7 @@ def compile_schema_storage_in_delta(
 def _compile_schema_storage_stmt(
     ctx: CompileContext,
     eql: str,
+    output_format: enums.OutputFormat = enums.OutputFormat.JSON,
 ) -> tuple[str, Sequence[dbstate.Param]]:
 
     schema = ctx.state.current_tx().get_schema(ctx.compiler_state.std_schema)
@@ -1529,7 +1530,7 @@ def _compile_schema_storage_stmt(
             state=ctx.state,
             json_parameters=True,
             schema_reflection_mode=True,
-            output_format=enums.OutputFormat.JSON,
+            output_format=output_format,
             expected_cardinality_one=False,
             bootstrap_mode=ctx.bootstrap_mode,
             protocol_version=ctx.protocol_version,
@@ -1746,23 +1747,31 @@ def _compile_ql_administer(
     script_info: Optional[irast.ScriptInfo] = None,
 ) -> dbstate.BaseQuery:
     if ql.expr.func == 'statistics_update':
-        return ddl.administer_statistics_update(ctx, ql)
+        res = ddl.administer_statistics_update(ctx, ql)
     elif ql.expr.func == 'schema_repair':
-        return ddl.administer_repair_schema(ctx, ql)
+        res = ddl.administer_repair_schema(ctx, ql)
     elif ql.expr.func == 'reindex':
-        return ddl.administer_reindex(ctx, ql)
+        res = ddl.administer_reindex(ctx, ql)
     elif ql.expr.func == 'vacuum':
-        return ddl.administer_vacuum(ctx, ql)
+        res = ddl.administer_vacuum(ctx, ql)
     elif ql.expr.func == 'prepare_upgrade':
-        return ddl.administer_prepare_upgrade(ctx, ql)
+        res = ddl.administer_prepare_upgrade(ctx, ql)
     elif ql.expr.func == '_remove_pointless_triggers':
-        return ddl.administer_remove_pointless_triggers(ctx, ql)
+        res = ddl.administer_remove_pointless_triggers(ctx, ql)
+    elif ql.expr.func == 'concurrent_index_create':
+        res = ddl.administer_concurrent_index_create(ctx, ql)
     else:
         raise errors.QueryError(
             'Unknown ADMINISTER function',
             span=ql.expr.span,
         )
 
+    if debug.flags.delta_execute or debug.flags.delta_execute_ddl:
+        debug.header('ADMINISTER script')
+        debug.dump_code(res.sql, lexer='sql')
+
+    return res
+
 
 def _compile_ql_query(
     ctx: CompileContext,
@@ -3086,6 +3095,7 @@ def _make_query_unit(
         unit.create_db_template = comp.create_db_template
         unit.create_db_mode = comp.create_db_mode
         unit.ddl_stmt_id = comp.ddl_stmt_id
+        unit.early_non_tx_sql = comp.early_non_tx_sql
         if not ctx.dump_restore_mode:
             if comp.user_schema is not None:
                 final_user_schema = comp.user_schema
diff --git a/edb/server/compiler/dbstate.py b/edb/server/compiler/dbstate.py
index 4a9348b2904..8dee8012924 100644
--- a/edb/server/compiler/dbstate.py
+++ b/edb/server/compiler/dbstate.py
@@ -174,6 +174,7 @@ class DDLQuery(BaseQuery):
     db_op_trailer: tuple[bytes, ...] = ()
     ddl_stmt_id: Optional[str] = None
     config_ops: list[config.Operation] = dataclasses.field(default_factory=list)
+    early_non_tx_sql: Optional[tuple[bytes, ...]] = None
 
 
 @dataclasses.dataclass(frozen=True, kw_only=True)
@@ -251,6 +252,9 @@ class QueryUnit:
     # If False, they will be executed separately.
     is_transactional: bool = True
 
+    # SQL to run *before* the main command, non transactionally
+    early_non_tx_sql: Optional[tuple[bytes, ...]] = None
+
     # Capabilities used in this query
     capabilities: enums.Capability = enums.Capability(0)
 
diff --git a/edb/server/compiler/ddl.py b/edb/server/compiler/ddl.py
index e212bd3b131..00bb5492e1e 100644
--- a/edb/server/compiler/ddl.py
+++ b/edb/server/compiler/ddl.py
@@ -23,6 +23,7 @@
 import dataclasses
 import json
 import textwrap
+import uuid
 
 from edb import errors
 
@@ -1839,6 +1840,108 @@ def administer_prepare_upgrade(
     )
 
 
+def _get_index(
+    ctx: compiler.CompileContext,
+    ql: qlast.AdministerStmt,
+) -> tuple[s_indexes.Index, s_schema.Schema]:
+    if len(ql.expr.args) != 1 or ql.expr.kwargs:
+        raise errors.QueryError(
+            f'{ql.expr.func}() takes exactly one positional argument',
+            span=ql.expr.span,
+        )
+
+    # This is janky, and we shouldn't do it.
+    arg = ql.expr.args[0]
+    match arg:
+        case qlast.Constant(
+            kind=qlast.ConstantKind.STRING,
+            value=id_string,
+        ):
+            pass
+        case _:
+            raise errors.QueryError(
+                f'argument to {ql.expr.func}() must be a string literal',
+                span=arg.span,
+            )
+
+    try:
+        id = uuid.UUID(id_string)
+    except ValueError:
+        raise errors.QueryError("Invalid index id")
+
+    user_schema = ctx.state.current_tx().get_user_schema()
+    global_schema = ctx.state.current_tx().get_global_schema()
+
+    schema = s_schema.ChainedSchema(
+        ctx.compiler_state.std_schema,
+        user_schema,
+        global_schema
+    )
+
+    index = schema.get_by_id(id, type=s_indexes.Index)
+    return index, schema
+
+
+def administer_concurrent_index_create(
+    ctx: compiler.CompileContext,
+    ql: qlast.AdministerStmt,
+) -> dbstate.BaseQuery:
+    index, schema = _get_index(ctx, ql)
+
+    if not index.get_create_concurrently(schema):
+        raise errors.QueryError("Index was not created concurrently")
+    if index.get_active(schema):
+        raise errors.QueryError("Index is already active")
+
+    delta_context = _new_delta_context(ctx)
+
+    create_index = pg_delta.CreateIndex.create_index(
+        index, schema, delta_context
+    )
+
+    block = pg_dbops.SQLBlock()
+    block.set_non_transactional()
+    create_index.generate(block)
+    # HACK: Separate out the real index command and the comments
+    # (where do the comments even get done??)
+    assert isinstance(block.commands[0], pg_dbops.PLBlock)
+    statements = block.commands[0].get_statements()
+    index_command, comments = statements
+
+    # Update the schema::Index to set `active = true`
+    block = pg_dbops.PLTopBlock()
+    context = s_delta.CommandContext()
+    delta_root = s_delta.DeltaRoot()
+    root, alter_index, _ = index.init_delta_branch(
+        schema, context, s_delta.AlterObject
+    )
+    alter_index.set_attribute_value('active', True)
+    delta_root.add(root)
+
+    nschema = delta_root.apply(schema, context)
+
+    # Construct the command
+    compiler.compile_schema_storage_in_delta(ctx, delta_root, block, context)
+    block.add_command(comments)
+    sql = block.to_string().encode('utf-8')
+
+    if debug.flags.delta_execute_ddl:
+        debug.header('ADMINISTER concurrent_index_create(...)')
+        debug.dump_code(index_command, lexer='sql')
+        debug.dump_code(sql, lexer='sql')
+
+    assert isinstance(nschema, s_schema.ChainedSchema)
+    assert isinstance(nschema._top_schema, s_schema.FlatSchema)
+
+    return dbstate.DDLQuery(
+        early_non_tx_sql=(index_command.encode('utf-8'),),
+        sql=sql,
+        is_transactional=False,
+        feature_used_metrics=None,
+        user_schema=nschema._top_schema,
+    )
+
+
 def validate_schema_equivalence(
     state: compiler.CompilerState,
     schema_a: s_schema.FlatSchema,
diff --git a/edb/server/protocol/execute.pyx b/edb/server/protocol/execute.pyx
index bbd2d3c6b35..1bdd4a5ab41 100644
--- a/edb/server/protocol/execute.pyx
+++ b/edb/server/protocol/execute.pyx
@@ -286,6 +286,13 @@ async def execute(
                 dbv.dbname,
                 close_frontend_conns=query_unit.drop_db_reset_connections,
             )
+
+        if query_unit.early_non_tx_sql:
+            # Sync state non transactionally
+            await be_conn.sql_fetch(b'select 1', state=state)
+            for sql in query_unit.early_non_tx_sql:
+                await be_conn.sql_execute(sql)
+
         if query_unit.system_config:
             # execute_system_config() always sync state in a separate tx,
             # so we don't need to pass down the needs_commit_state here
diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py
index a979712a244..5a815c2c094 100644
--- a/tests/test_edgeql_ddl.py
+++ b/tests/test_edgeql_ddl.py
@@ -19870,6 +19870,10 @@ async def test_edgeql_ddl_alias_and_create_set_required(self):
 class TestDDLNonIsolated(tb.DDLTestCase):
     TRANSACTION_ISOLATION = False
 
+    PER_TEST_TEARDOWN = [
+        'reset schema to initial'
+    ]
+
     async def test_edgeql_ddl_consecutive_create_migration_01(self):
         # A regression test for https://github.com/edgedb/edgedb/issues/2085.
         await self.con.execute('''
@@ -20163,3 +20167,127 @@ async def test_edgeql_ddl_rollback_enum_01(self):
             """,
             [True],
         )
+
+    async def test_edgeql_ddl_concurrent_index_01(self):
+        await self.con.execute('''
+            create type T { create required property n -> str; };
+        ''')
+        await self.con.execute('''
+            alter type T {
+                create index on (.n) { set create_concurrently := true; }
+            };
+        ''')
+        with self.assertRaisesRegex(edgedb.SchemaDefinitionError, 'not active'):
+            await self.con.execute('''
+                alter type T {
+                    alter index on (.n) { set create_concurrently := false; }
+                };
+            ''')
+
+        msgs = []
+        await create_concurrent_indexes(self.con, msg_callback=msgs.append)
+        self.assertEqual(
+            msgs, ["Creating concurrent index on 'default::T' with expr (.n)"]
+        )
+
+        await self.con.execute('''
+            alter type T {
+                alter index on (.n) { set create_concurrently := false; }
+            };
+        ''')
+
+    async def test_edgeql_ddl_concurrent_index_02(self):
+        await self.con.execute('''
+            create type T {
+                create required property n -> str;
+                create required property s -> int64;
+                create index on (.n) { set create_concurrently := true; };
+                create index on (.s) { set create_concurrently := true; };
+            };
+        ''')
+
+        msgs = []
+        await create_concurrent_indexes(self.con, msg_callback=msgs.append)
+        msgs.sort()
+        self.assertEqual(
+            msgs,
+            [
+                "Creating concurrent index on 'default::T' with expr (.n)",
+                "Creating concurrent index on 'default::T' with expr (.s)",
+            ]
+        )
+
+    async def test_edgeql_ddl_concurrent_index_03(self):
+        await self.con.execute('''
+            create type T { create required property n -> str; };
+        ''')
+        await self.con.execute('''
+            alter type T {
+                create index on (.n) { set create_concurrently := true; }
+            };
+        ''')
+
+        ev = asyncio.Event()
+
+        con2 = await self.connect(database=self.con.dbname)
+        try:
+            async with self.con.transaction():
+                await self.con.execute('''
+                    insert T { n := "0123" };
+                ''')
+                task = asyncio.create_task(
+                    create_concurrent_indexes(
+                        con2,
+                        msg_callback=lambda _: ev.set()
+                    )
+                )
+                await ev.wait()
+                await asyncio.sleep(2)
+
+                await self.con.execute('''
+                    insert T { n := "789" };
+                ''')
+
+                # Task can't have finished while the tx is held open
+                assert not task.done()
+
+            self.assertEqual(await task, 1)
+        finally:
+            await con2.aclose()
+
+        self.assertEqual(
+            await create_concurrent_indexes(
+                self.con, msg_callback=lambda _: None
+            ),
+            0,
+        )
+
+
+async def create_concurrent_indexes(db, msg_callback=print):
+    '''Actually create all "create concurrently" indexes
+
+    The protocol here is to find all the indexes that need created,
+    and create them with `administer concurrent_index_create()`.
+    It's possible that the database will shut down after an index
+    creation but before the metadata is updated, in which case
+    we might rerun the command later, which is harmless.
+
+    If we stick with this ADMINISTER-based schemed, I figure this code
+    would live in the CLI.
+    '''
+    indexes = await db.query('''
+        select schema::Index {
+            id, expr, subject_name := .<indexes[is schema::ObjectType].name
+        }
+        filter .create_concurrently and not .active
+    ''')
+    for index in indexes:
+        msg_callback(
+            f"Creating concurrent index on '{index.subject_name}' "
+            f"with expr ({index.expr})"
+        )
+        await db.execute(f'''
+            administer concurrent_index_create("{index.id}")
+        ''')
+
+    return len(indexes)


Code review comments:
diff_hunk: @@ -1839,6 +1840,108 @@ def administer_prepare_upgrade(
     )
 
 
+def _get_index(
+    ctx: compiler.CompileContext,
+    ql: qlast.AdministerStmt,
+) -> tuple[s_indexes.Index, s_schema.Schema]:
+    if len(ql.expr.args) != 1 or ql.expr.kwargs:
+        raise errors.QueryError(
+            f'{ql.expr.func}() takes exactly one positional argument',
+            span=ql.expr.span,
+        )
+
+    # This is janky, and we shouldn't do it.
+    arg = ql.expr.args[0]
+    match arg:
+        case qlast.Constant(
+            kind=qlast.ConstantKind.STRING,
+            value=id_string,
+        ):
+            pass
+        case _:
+            raise errors.QueryError(
+                f'argument to {ql.expr.func}() must be a string literal',
+                span=arg.span,
+            )
path: edb/server/compiler/ddl.py
text: @user1:
String literal? How hard would it be to accept a `<uuid>$0`?

Does not really matter

@author:
I can make it support `<uuid>"string literal"` but supporting `$0` is kind of hard, since arguments never go to the compiler.

@author:
I've made it support and require `<uuid>"..."` instead. Still kind of a bummer but less of a hack