| | | 1 | | namespace AsyncResponse.Internal; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Pairwise distinctness for a store's effective object-name plan: the configured names plus every |
| | | 5 | | /// index and sequence name derived from them. Derived-name helpers reserve suffix space by |
| | | 6 | | /// truncating the stem, so two long configured names can still derive one name — and a configured |
| | | 7 | | /// name can occupy a derived one outright — after which <c>CREATE ... IF NOT EXISTS</c> (or SQL |
| | | 8 | | /// Server's <c>IF OBJECT_ID(...) IS NULL</c>) silently skips the object instead of failing. |
| | | 9 | | /// Source-linked into the packages that own such a plan (separate packages cannot share compiled |
| | | 10 | | /// code); the caller supplies the guidance tail so each options type keeps naming its own objects. |
| | | 11 | | /// </summary> |
| | | 12 | | internal static class RelationalNamePlan |
| | | 13 | | { |
| | | 14 | | /// <summary> |
| | | 15 | | /// Throws when two entries resolve to the same name. Comparison is case-insensitive even where |
| | | 16 | | /// the DDL quotes identifiers (case-sensitive to PostgreSQL): a plan distinct only by letter |
| | | 17 | | /// case is a misconfiguration magnet, and SQL Server's catalogs fold case by default anyway. |
| | | 18 | | /// </summary> |
| | | 19 | | public static void RequireDistinct((string Role, string Name)[] plan, string optionsName, string guidance) |
| | | 20 | | { |
| | 6856 | 21 | | for (var i = 0; i < plan.Length; i++) |
| | | 22 | | { |
| | 10280 | 23 | | for (var j = i + 1; j < plan.Length; j++) |
| | | 24 | | { |
| | 2572 | 25 | | if (string.Equals(plan[i].Name, plan[j].Name, StringComparison.OrdinalIgnoreCase)) |
| | | 26 | | { |
| | | 27 | | // BOTH spellings, not just one: the comparison folds case, so a collision can |
| | | 28 | | // exist between names the operator's configuration shows as different |
| | | 29 | | // ("Messages" vs "messages"). Reporting a single spelling made that error read |
| | | 30 | | // as a false positive, with nothing pointing at the case-fold rule above. |
| | 4 | 31 | | throw new InvalidOperationException( |
| | 4 | 32 | | $"{optionsName}: the {plan[i].Role} ('{plan[i].Name}') and the {plan[j].Role} ('{plan[j].Name}') |
| | 4 | 33 | | $"the same name — object names are compared case-insensitively" + guidance); |
| | | 34 | | } |
| | | 35 | | } |
| | | 36 | | } |
| | 856 | 37 | | } |
| | | 38 | | |
| | | 39 | | /// <summary> |
| | | 40 | | /// A derived object name with the suffix space RESERVED before the provider's identifier cap: |
| | | 41 | | /// the stem is truncated, never the composed name. Truncating "{stem}{suffix}" as a whole lets |
| | | 42 | | /// a maximum-length table derive its OWN name, after which <c>CREATE ... IF NOT EXISTS</c> (or |
| | | 43 | | /// SQL Server's <c>IF OBJECT_ID(...) IS NULL</c> / <c>sys.indexes</c> guard) matches the |
| | | 44 | | /// existing object and silently creates nothing — and two derived names can collapse onto one |
| | | 45 | | /// another the same way. |
| | | 46 | | /// <para> |
| | | 47 | | /// The single implementation of a rule that was previously copy-pasted across the channel and |
| | | 48 | | /// transport stores, each with its own cap constant. Callers pass their provider's cap. |
| | | 49 | | /// </para> |
| | | 50 | | /// </summary> |
| | | 51 | | /// <exception cref="ArgumentOutOfRangeException"> |
| | | 52 | | /// <paramref name="suffix"/> leaves no room for a stem inside <paramref name="identifierCap"/>. |
| | | 53 | | /// Guarded explicitly: the slice below would otherwise take a negative length and fail schema |
| | | 54 | | /// creation with a bare index-out-of-range that names neither the suffix nor the cap. |
| | | 55 | | /// </exception> |
| | | 56 | | public static string DerivedName(string stem, string suffix, int identifierCap) |
| | | 57 | | { |
| | 2566 | 58 | | if (identifierCap <= 0 || stem.Length + suffix.Length <= identifierCap) |
| | 2550 | 59 | | return stem + suffix; |
| | | 60 | | |
| | 16 | 61 | | if (suffix.Length >= identifierCap) |
| | | 62 | | { |
| | 2 | 63 | | throw new ArgumentOutOfRangeException( |
| | 2 | 64 | | nameof(suffix), |
| | 2 | 65 | | $"The derived-name suffix '{suffix}' is {suffix.Length} characters, which leaves no room for a stem insi |
| | 2 | 66 | | $"{identifierCap}-character identifier limit. Shorten the suffix."); |
| | | 67 | | } |
| | | 68 | | |
| | 14 | 69 | | return stem[..(identifierCap - suffix.Length)] + suffix; |
| | | 70 | | } |
| | | 71 | | } |