< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.Internal.PostgreSqlRelationVerifier
Assembly: AsyncResponse.DurableFlows.PostgreSQL
File(s): /_/src/Shared/PostgreSqlRelationVerifier.cs
Line coverage
77%
Covered lines: 140
Uncovered lines: 41
Coverable lines: 181
Total lines: 423
Line coverage: 77.3%
Branch coverage
67%
Covered branches: 80
Total branches: 118
Branch coverage: 67.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Name()100%11100%
get_Type()100%11100%
get_Nullable()100%11100%
get_RequiresDeterministicCollation()100%11100%
get_DefaultExpression()100%11100%
get_Name()100%11100%
get_Kind()100%11100%
get_OwningTable()100%11100%
get_KeyColumns()100%11100%
get_Columns()100%11100%
get_PrimaryKey()100%11100%
VerifyAsync()100%11100%
LoadRelationsAsync()100%22100%
LoadTableColumnsAsync()75%4494.44%
Evaluate(...)60.86%1364665.11%
EvaluateTableColumns(...)78.57%834271.42%
VerifySequence(...)0%156120%
DdlCollisionMessage(...)100%210%
DescribeKind(...)40%191055.55%
get_Kind()100%11100%
get_Persistence()100%11100%
get_OwningTable()100%11100%
get_AccessMethod()100%11100%
get_IsUnique()100%11100%
get_HasPredicate()100%11100%
get_IsValidAndReady()100%11100%
get_KeyColumns()100%11100%
get_SequenceType()100%210%
get_SequenceIncrement()100%210%
get_SequenceCache()100%210%
get_SequenceCycles()100%210%
get_SequenceMax()100%210%
get_PrimaryKey()100%11100%
get_Type()100%11100%
get_NotNull()100%11100%
get_Default()100%11100%
get_Writable()100%11100%
get_Collation()100%11100%
get_DeterministicCollation()100%11100%

File(s)

/_/src/Shared/PostgreSqlRelationVerifier.cs

#LineLine coverage
 1using Npgsql;
 2
 3namespace AsyncResponse.Internal;
 4
 5/// <summary>
 6/// In-transaction catalog verification that every relation a PostgreSQL store just ensured
 7/// actually IS what its DDL intended. <c>CREATE ... IF NOT EXISTS</c> matches ANY relation with
 8/// the name (tables, indexes, and sequences share one namespace per schema) and explicitly
 9/// guarantees nothing about an existing object's shape — a same-name table missing operational
 10/// columns, an index with different key columns, uniqueness, a predicate or access method, or a
 11/// sequence with the wrong increment/cache/cycle/bounds is silently accepted by the DDL and
 12/// fails (or corrupts ordering) only at runtime. Runs under the schema-keyed advisory DDL lock
 13/// shared by every AsyncResponse PostgreSQL store, so other components' objects are either
 14/// committed and visible or serialized behind this transaction. Source-linked into the channel,
 15/// transport, and durable-flow packages (separate packages cannot share compiled code).
 16/// </summary>
 17internal static class PostgreSqlRelationVerifier
 18{
 19    /// <summary>
 20    /// One expected table column: name, <c>format_type</c> rendering, nullability, and — for
 21    /// columns whose DDL declares a default the runtime relies on — the exact
 22    /// <c>pg_get_expr</c> rendering of that default. A merely EXISTING default is not enough:
 23    /// <c>created_at DEFAULT now() + interval '1 year'</c> silently shifts every timestamp the
 24    /// watermark and visibility logic compare, and <c>available_at</c> with a future default
 25    /// would strand transport jobs.
 26    /// <para>
 27    /// <paramref name="RequiresDeterministicCollation"/> marks the columns that store an identity
 28    /// the library compares ORDINALLY — correlation ids, queue names, flow ids. Under a
 29    /// non-deterministic ICU collation the database treats strings its own rules call equal as ONE
 30    /// key, so two distinct ids collide: lookups cross-match and a primary key rejects the second
 31    /// id. A type that carries no collation at all (<c>uuid</c>, <c>bigint</c>) reports none and
 32    /// always passes.
 33    /// </para>
 34    /// </summary>
 35    /// <remarks>
 36    /// The check reads the column's OWN collation. A DATABASE whose default collation is itself
 37    /// non-deterministic is out of scope: an uncollated declaration records the
 38    /// <c>pg_catalog."default"</c> entry, which is always marked deterministic whatever the
 39    /// cluster's locale, so the catalog cannot answer that question here.
 40    /// </remarks>
 41    internal readonly record struct ExpectedColumn(
 192842        string Name,
 96743        string Type,
 96544        bool Nullable,
 96345        bool RequiresDeterministicCollation = false,
 110046        string? DefaultExpression = null);
 47
 48    /// <summary>
 49    /// One expected relation: kind 'r' (table, verified against <paramref name="Columns"/> and
 50    /// <paramref name="PrimaryKey"/> when given), 'S' (sequence, verified <c>bigint</c>,
 51    /// increment 1, cache 1, no cycle, full positive range), or 'i' (index, verified to sit on
 52    /// <paramref name="OwningTable"/> as a plain — non-unique, non-partial, valid and ready —
 53    /// btree over exactly <paramref name="KeyColumns"/> in order). All relations must be
 54    /// permanent (not UNLOGGED or temporary).
 55    /// </summary>
 56    internal readonly record struct ExpectedRelation(
 309557        string Name,
 173858        char Kind,
 42959        string? OwningTable = null,
 13560        string[]? KeyColumns = null,
 59661        ExpectedColumn[]? Columns = null,
 15762        string[]? PrimaryKey = null);
 63
 64    public static async Task VerifyAsync(
 65        NpgsqlConnection connection,
 66        NpgsqlTransaction? transaction,
 67        string schemaName,
 68        string componentName,
 69        IReadOnlyList<ExpectedRelation> expected,
 70        CancellationToken cancellationToken)
 71    {
 13772        var relations = await LoadRelationsAsync(connection, transaction, schemaName, expected, cancellationToken).Confi
 13773        var columns = await LoadTableColumnsAsync(connection, transaction, schemaName, expected, cancellationToken).Conf
 13774        Evaluate(schemaName, componentName, expected, relations, columns);
 13475    }
 76
 77    // Both primary-key columns and index key columns come from indkey sliced to indnkeyatts:
 78    // indkey lists key columns FOLLOWED by INCLUDE payload columns, and a covering
 79    // PRIMARY KEY (…) INCLUDE (…) enforces exactly the uniqueness the stores rely on — reading
 80    // the whole vector would reject it for carrying its payload columns.
 81    internal const string RelationQuery =
 82        """
 83        SELECT c.relname,
 84               c.relkind::text,
 85               c.relpersistence::text,
 86               COALESCE(t.relname, ''),
 87               COALESCE(am.amname, ''),
 88               COALESCE(i.indisunique, false),
 89               i.indpred IS NOT NULL,
 90               COALESCE(i.indisvalid AND i.indisready, true),
 91               COALESCE((SELECT array_agg(a.attname ORDER BY k.ord)
 92                         FROM unnest(i.indkey[0:i.indnkeyatts-1]) WITH ORDINALITY AS k(attnum, ord)
 93                         JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum), '{}'),
 94               COALESCE(s.seqtypid::regtype::text, ''),
 95               COALESCE(s.seqincrement, 1),
 96               COALESCE(s.seqcache, 1),
 97               COALESCE(s.seqcycle, false),
 98               COALESCE(s.seqmax, 9223372036854775807),
 99               COALESCE((SELECT array_agg(a2.attname ORDER BY k2.ord)
 100                         FROM pg_index pi
 101                         CROSS JOIN LATERAL unnest(pi.indkey[0:pi.indnkeyatts-1]) WITH ORDINALITY AS k2(attnum, ord)
 102                         JOIN pg_attribute a2 ON a2.attrelid = c.oid AND a2.attnum = k2.attnum
 103                         WHERE pi.indrelid = c.oid AND pi.indisprimary), '{}')
 104        FROM pg_class c
 105        JOIN pg_namespace n ON n.oid = c.relnamespace
 106        LEFT JOIN pg_index i ON i.indexrelid = c.oid
 107        LEFT JOIN pg_class t ON t.oid = i.indrelid
 108        LEFT JOIN pg_am am ON am.oid = c.relam
 109        LEFT JOIN pg_sequence s ON s.seqrelid = c.oid
 110        WHERE n.nspname = @schema AND c.relname = ANY(@names);
 111        """;
 112
 113    private static async Task<Dictionary<string, ActualRelation>> LoadRelationsAsync(
 114        NpgsqlConnection connection,
 115        NpgsqlTransaction? transaction,
 116        string schemaName,
 117        IReadOnlyList<ExpectedRelation> expected,
 118        CancellationToken cancellationToken)
 119    {
 137120        await using var verify = connection.CreateCommand();
 137121        verify.Transaction = transaction;
 137122        verify.CommandText = RelationQuery;
 137123        verify.Parameters.AddWithValue("schema", schemaName);
 411124        verify.Parameters.AddWithValue("names", expected.Select(e => e.Name).ToArray());
 125
 137126        var actual = new Dictionary<string, ActualRelation>(StringComparer.Ordinal);
 137127        await using var reader = await verify.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
 409128        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
 129        {
 272130            actual[reader.GetString(0)] = new ActualRelation(
 272131                Kind: reader.GetString(1),
 272132                Persistence: reader.GetString(2),
 272133                OwningTable: reader.GetString(3),
 272134                AccessMethod: reader.GetString(4),
 272135                IsUnique: reader.GetBoolean(5),
 272136                HasPredicate: reader.GetBoolean(6),
 272137                IsValidAndReady: reader.GetBoolean(7),
 272138                KeyColumns: reader.GetFieldValue<string[]>(8),
 272139                SequenceType: reader.GetString(9),
 272140                SequenceIncrement: reader.GetInt64(10),
 272141                SequenceCache: reader.GetInt64(11),
 272142                SequenceCycles: reader.GetBoolean(12),
 272143                SequenceMax: reader.GetInt64(13),
 272144                PrimaryKey: reader.GetFieldValue<string[]>(14));
 145        }
 146
 137147        return actual;
 137148    }
 149
 150    // The writable column is "writable without being named": a default (pg_attrdef also carries
 151    // stored generation expressions), an identity, or a generated column. Identity columns have
 152    // NO pg_attrdef row, so testing the rendered default alone would misread
 153    // GENERATED ... AS IDENTITY — which PostgreSQL populates on every insert — as unwritable.
 154    // format_type renders no collation, so the last two columns join it in separately;
 155    // attcollation is 0 for a type that cannot carry one, which the LEFT JOIN resolves to the
 156    // deterministic default rather than a missing row that would fail every flagged column.
 157    internal const string TableColumnQuery =
 158        """
 159        SELECT c.relname, a.attname, format_type(a.atttypid, a.atttypmod), a.attnotnull,
 160               COALESCE(pg_get_expr(ad.adbin, ad.adrelid), ''),
 161               ad.adrelid IS NOT NULL OR a.attidentity <> '' OR a.attgenerated <> '',
 162               COALESCE(co.collname, ''),
 163               COALESCE(co.collisdeterministic, true)
 164        FROM pg_class c
 165        JOIN pg_namespace n ON n.oid = c.relnamespace
 166        JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
 167        LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
 168        LEFT JOIN pg_collation co ON co.oid = a.attcollation
 169        WHERE n.nspname = @schema AND c.relname = ANY(@names);
 170        """;
 171
 172    private static async Task<Dictionary<(string Table, string Column), ActualColumn>> LoadTableColumnsAsync(
 173        NpgsqlConnection connection,
 174        NpgsqlTransaction? transaction,
 175        string schemaName,
 176        IReadOnlyList<ExpectedRelation> expected,
 177        CancellationToken cancellationToken)
 178    {
 137179        var actual = new Dictionary<(string Table, string Column), ActualColumn>();
 411180        var tables = expected.Where(e => e.Kind == 'r' && e.Columns is not null).ToArray();
 137181        if (tables.Length == 0)
 0182            return actual;
 183
 137184        await using var verify = connection.CreateCommand();
 137185        verify.Transaction = transaction;
 137186        verify.CommandText = TableColumnQuery;
 137187        verify.Parameters.AddWithValue("schema", schemaName);
 274188        verify.Parameters.AddWithValue("names", tables.Select(t => t.Name).ToArray());
 189
 137190        await using var reader = await verify.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
 1096191        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
 192        {
 959193            actual[(reader.GetString(0), reader.GetString(1))] = new ActualColumn(
 959194                Type: reader.GetString(2),
 959195                NotNull: reader.GetBoolean(3),
 959196                Default: reader.GetString(4),
 959197                Writable: reader.GetBoolean(5),
 959198                Collation: reader.GetString(6),
 959199                DeterministicCollation: reader.GetBoolean(7));
 200        }
 201
 137202        return actual;
 137203    }
 204
 205    internal static void Evaluate(
 206        string schemaName,
 207        string componentName,
 208        IReadOnlyList<ExpectedRelation> expected,
 209        Dictionary<string, ActualRelation> relations,
 210        Dictionary<(string Table, string Column), ActualColumn> columns)
 211    {
 212        // Diagnose in CAUSE order, not declaration order. A misprovisioned or colliding schema is
 213        // usually wrong in several ways at once — a foreign relation occupying one name is WHY a
 214        // dependent object was never created, and an operator who mis-shaped a table has typically
 215        // also forgotten an index — so reporting "does not exist" first would name a victim and
 216        // hide the culprit. Anything that is present and wrong is checked first; absence is only
 217        // reported once nothing present explains it.
 914218        foreach (var relation in expected)
 219        {
 300220            if (!relations.TryGetValue(relation.Name, out var found))
 221                continue;
 222
 294223            if (found.Kind != relation.Kind.ToString()
 294224                || (relation.OwningTable is not null && !string.Equals(found.OwningTable, relation.OwningTable, StringCo
 225            {
 2226                var expectedDescription = relation.OwningTable is null
 2227                    ? DescribeKind(relation.Kind.ToString())
 2228                    : $"an index on '{relation.OwningTable}'";
 2229                var actualDescription = found.OwningTable.Length == 0
 2230                    ? DescribeKind(found.Kind)
 2231                    : $"an index on '{found.OwningTable}'";
 2232                throw new InvalidOperationException(
 2233                    $"The PostgreSQL {componentName} store expected '{schemaName}.{relation.Name}' to be {expectedDescri
 2234                    $"but the name is occupied by {actualDescription}, so CREATE ... IF NOT EXISTS silently skipped crea
 2235                    CollisionGuidance);
 236            }
 237
 292238            if (found.Persistence != "p")
 0239                throw new InvalidOperationException(
 0240                    $"The PostgreSQL {componentName} store's relation '{schemaName}.{relation.Name}' exists but is not a
 0241                    "(UNLOGGED or temporary): its content would not survive a crash or session end. Drop or convert it a
 242
 292243            if (relation.Kind == 'S')
 0244                VerifySequence(schemaName, componentName, relation.Name, found);
 245
 292246            if (relation.Kind == 'i' && relation.KeyColumns is { } keyColumns)
 247            {
 135248                if (!found.IsValidAndReady)
 0249                    throw new InvalidOperationException(
 0250                        $"The PostgreSQL {componentName} store's index '{schemaName}.{relation.Name}' exists but is inva
 0251                        "(a failed CREATE INDEX CONCURRENTLY leaves such an index behind). Drop it and restart so the st
 252
 135253                if (found.IsUnique || found.HasPredicate || found.AccessMethod != "btree" || !found.KeyColumns.AsSpan().
 0254                    throw new InvalidOperationException(
 0255                        $"The PostgreSQL {componentName} store's index '{schemaName}.{relation.Name}' exists but does no
 0256                        $"expected definition: expected a plain btree over ({string.Join(", ", keyColumns)}); found " +
 0257                        $"{(found.IsUnique ? "a UNIQUE " : "a ")}{(found.HasPredicate ? "partial " : "")}{found.AccessMe
 0258                        $"({string.Join(", ", found.KeyColumns)}). CREATE INDEX IF NOT EXISTS accepts ANY existing index
 0259                        "guarantees nothing about its shape — drop or rename the existing index so the store can create 
 260            }
 261
 292262            if (relation.Kind == 'r' && relation.PrimaryKey is { } primaryKey && !found.PrimaryKey.AsSpan().SequenceEqua
 2263                throw new InvalidOperationException(
 2264                    $"The PostgreSQL {componentName} store's table '{schemaName}.{relation.Name}' exists but its primary
 2265                    $"({string.Join(", ", found.PrimaryKey)}) instead of ({string.Join(", ", primaryKey)}). " + Collisio
 266        }
 267
 155268        EvaluateTableColumns(schemaName, componentName, expected, relations, columns);
 269
 867270        foreach (var relation in expected)
 271        {
 286272            if (!relations.ContainsKey(relation.Name))
 3273                throw new InvalidOperationException(
 3274                    $"The PostgreSQL {componentName} store expected '{schemaName}.{relation.Name}' to exist after schema
 3275                    "but it does not. " + CollisionGuidance);
 276        }
 146277    }
 278
 279    /// <summary>
 280    /// Column-level table verification: a same-kind table occupying the name — another
 281    /// component's, or a crafted one that happens to satisfy the index DDL — passes the relation
 282    /// check and fails only at the first INSERT/SELECT, or worse, silently changes runtime
 283    /// behavior. Every DDL-declared column must exist with the declared type and nullability;
 284    /// columns the DDL gives runtime-relied defaults must carry EXACTLY that default expression
 285    /// (a same-named default computing something else shifts every timestamp the store
 286    /// compares); identity columns must carry a deterministic collation (the SQL Server sibling's
 287    /// binary-collation rule, in the form PostgreSQL expresses it); and extra columns are allowed
 288    /// only when they are writable without being named
 289    /// (nullable, defaulted, identity, or generated) — an extra NOT NULL column the database
 290    /// cannot fill in itself fails every normal insert with 23502.
 291    /// </summary>
 292    private static void EvaluateTableColumns(
 293        string schemaName,
 294        string componentName,
 295        IReadOnlyList<ExpectedRelation> expected,
 296        Dictionary<string, ActualRelation> relations,
 297        Dictionary<(string Table, string Column), ActualColumn> columns)
 298    {
 888299        foreach (var table in expected)
 300        {
 292301            if (table.Kind != 'r' || table.Columns is null || !relations.ContainsKey(table.Name))
 302                continue;
 303
 2232304            foreach (var column in table.Columns)
 305            {
 965306                if (!columns.TryGetValue((table.Name, column.Name), out var found))
 0307                    throw new InvalidOperationException(
 0308                        $"The PostgreSQL {componentName} store's table '{schemaName}.{table.Name}' exists but is missing
 0309                        $"'{column.Name}' ({column.Type}); a same-name table from another component or a partial manual 
 0310                        "occupies the name. " + CollisionGuidance);
 311
 965312                if (!string.Equals(found.Type, column.Type, StringComparison.Ordinal)
 965313                    || found.NotNull == column.Nullable
 965314                    || (column.DefaultExpression is not null && !string.Equals(found.Default, column.DefaultExpression, 
 315                {
 2316                    throw new InvalidOperationException(
 2317                        $"The PostgreSQL {componentName} store's table '{schemaName}.{table.Name}' exists but column '{c
 2318                        $"does not match the expected shape: expected {column.Type}{(column.Nullable ? " NULL" : " NOT N
 2319                        $"{(column.DefaultExpression is null ? "" : $" DEFAULT {column.DefaultExpression}")}; found {fou
 2320                        $"{(found.NotNull ? " NOT NULL" : " NULL")}{(found.Default.Length == 0 ? " without a default" : 
 2321                        CollisionGuidance);
 322                }
 323
 963324                if (column.RequiresDeterministicCollation && !found.DeterministicCollation)
 2325                    throw new InvalidOperationException(
 2326                        $"The PostgreSQL {componentName} store's column '{schemaName}.{table.Name}.{column.Name}' uses t
 2327                        $"collation '{found.Collation}', which is non-deterministic (collisdeterministic = false). That 
 2328                        "stores an identity the library compares ORDINALLY, and a non-deterministic collation folds what
 2329                        "rules call equal — case, accents, or full-width forms, depending on the ICU rule — into one key
 2330                        "ids would then collide: lookups cross-match and the second id is rejected on insert. ALTER the 
 2331                        "a deterministic collation (\"C\" compares by code point) after dropping the keys and indexes th
 2332                        "reference it.");
 333            }
 334
 335            // Extra columns are fine only when inserts that do not name them can still succeed.
 1108336            var expectedNames = table.Columns.Select(c => c.Name).ToHashSet(StringComparer.Ordinal);
 2230337            foreach (var ((tableName, columnName), found) in columns)
 338            {
 967339                if (!string.Equals(tableName, table.Name, StringComparison.Ordinal) || expectedNames.Contains(columnName
 340                    continue;
 341
 8342                if (found.NotNull && !found.Writable)
 2343                    throw new InvalidOperationException(
 2344                        $"The PostgreSQL {componentName} store's table '{schemaName}.{table.Name}' has an extra column "
 2345                        $"'{columnName}' that is NOT NULL without a default: every insert the store issues would fail wi
 2346                        "not_null_violation (23502). Make the column nullable, give it a default, or drop it.");
 347            }
 348        }
 149349    }
 350
 351    /// <summary>
 352    /// The ack sequence is a cross-process monotonic clock: delivery claims and waiter
 353    /// registrations draw from it and compare positions, so any property that lets drawn values
 354    /// go backwards or repeat silently corrupts the same-tick tie-breaker — a descending
 355    /// increment counts down, CYCLE wraps, and CACHE &gt; 1 hands each session a private block
 356    /// so cross-session draw order no longer matches value order.
 357    /// </summary>
 358    private static void VerifySequence(string schemaName, string componentName, string name, ActualRelation found)
 359    {
 0360        if (found.SequenceType != "bigint"
 0361            || found.SequenceIncrement != 1
 0362            || found.SequenceCache != 1
 0363            || found.SequenceCycles
 0364            || found.SequenceMax != long.MaxValue)
 365        {
 0366            throw new InvalidOperationException(
 0367                $"The PostgreSQL {componentName} store's sequence '{schemaName}.{name}' exists but does not behave as th
 0368                $"cross-process monotonic clock: expected bigint, INCREMENT 1, CACHE 1, NO CYCLE, MAXVALUE {long.MaxValu
 0369                $"{found.SequenceType}, INCREMENT {found.SequenceIncrement}, CACHE {found.SequenceCache}, " +
 0370                $"{(found.SequenceCycles ? "CYCLE" : "NO CYCLE")}, MAXVALUE {found.SequenceMax}. Fix it with " +
 0371                $"ALTER SEQUENCE \"{schemaName}\".\"{name}\" AS bigint INCREMENT 1 CACHE 1 NO CYCLE NO MAXVALUE; and res
 372        }
 0373    }
 374
 375    /// <summary>
 376    /// Guidance appended when the schema-object DDL itself fails on an occupied name
 377    /// (SQLSTATE 42809 wrong object type, or 42703 undefined column when a same-kind foreign
 378    /// table made IF NOT EXISTS skip the create and the dependent index DDL then referenced a
 379    /// column that table does not have).
 380    /// </summary>
 381    public static string DdlCollisionMessage(string componentName, string schemaName)
 0382        => $"The PostgreSQL {componentName} store could not create its schema objects in '{schemaName}' because a config
 0383           "derived name is occupied by an object of a different kind or shape. " + CollisionGuidance;
 384
 385    private const string CollisionGuidance =
 386        "Tables, indexes, and sequences share one namespace per schema — across the channel, transport, and durable-flow
 387        "stores and any unrelated objects in it. Rename the configured tables so every configured and derived name stays
 388        "unique within the schema.";
 389
 4390    private static string DescribeKind(string kind) => kind switch
 4391    {
 2392        "r" => "a table",
 0393        "i" => "an index",
 2394        "S" => "a sequence",
 0395        "v" => "a view",
 0396        "m" => "a materialized view",
 0397        _ => $"a relation of kind '{kind}'",
 4398    };
 399
 400    internal readonly record struct ActualRelation(
 296401        string Kind,
 292402        string Persistence,
 137403        string OwningTable,
 135404        string AccessMethod,
 135405        bool IsUnique,
 135406        bool HasPredicate,
 135407        bool IsValidAndReady,
 135408        string[] KeyColumns,
 0409        string SequenceType,
 0410        long SequenceIncrement,
 0411        long SequenceCache,
 0412        bool SequenceCycles,
 0413        long SequenceMax,
 141414        string[] PrimaryKey);
 415
 416    internal readonly record struct ActualColumn(
 967417        string Type,
 973418        bool NotNull,
 137419        string Default,
 6420        bool Writable,
 2421        string Collation,
 143422        bool DeterministicCollation);
 423}

Methods/Properties

get_Name()
get_Type()
get_Nullable()
get_RequiresDeterministicCollation()
get_DefaultExpression()
get_Name()
get_Kind()
get_OwningTable()
get_KeyColumns()
get_Columns()
get_PrimaryKey()
VerifyAsync()
LoadRelationsAsync()
LoadTableColumnsAsync()
Evaluate(System.String,System.String,System.Collections.Generic.IReadOnlyList`1<AsyncResponse.Internal.PostgreSqlRelationVerifier/ExpectedRelation>,System.Collections.Generic.Dictionary`2<System.String,AsyncResponse.Internal.PostgreSqlRelationVerifier/ActualRelation>,System.Collections.Generic.Dictionary`2<System.ValueTuple`2<System.String,System.String>,AsyncResponse.Internal.PostgreSqlRelationVerifier/ActualColumn>)
EvaluateTableColumns(System.String,System.String,System.Collections.Generic.IReadOnlyList`1<AsyncResponse.Internal.PostgreSqlRelationVerifier/ExpectedRelation>,System.Collections.Generic.Dictionary`2<System.String,AsyncResponse.Internal.PostgreSqlRelationVerifier/ActualRelation>,System.Collections.Generic.Dictionary`2<System.ValueTuple`2<System.String,System.String>,AsyncResponse.Internal.PostgreSqlRelationVerifier/ActualColumn>)
VerifySequence(System.String,System.String,System.String,AsyncResponse.Internal.PostgreSqlRelationVerifier/ActualRelation)
DdlCollisionMessage(System.String,System.String)
DescribeKind(System.String)
get_Kind()
get_Persistence()
get_OwningTable()
get_AccessMethod()
get_IsUnique()
get_HasPredicate()
get_IsValidAndReady()
get_KeyColumns()
get_SequenceType()
get_SequenceIncrement()
get_SequenceCache()
get_SequenceCycles()
get_SequenceMax()
get_PrimaryKey()
get_Type()
get_NotNull()
get_Default()
get_Writable()
get_Collation()
get_DeterministicCollation()