|
5 | 5 | canonicalizeSqlType, |
6 | 6 | suggestFieldTypeForSqlType, |
7 | 7 | isCompatible, |
| 8 | + type SqlDialect, |
8 | 9 | } from './type-compat'; |
9 | 10 | import { suggestFieldType } from '../shared/suggestions.zod'; |
10 | 11 |
|
@@ -154,3 +155,88 @@ describe('isCompatible', () => { |
154 | 155 | expect(isCompatible('geometry', 'number')).toBe(false); |
155 | 156 | }); |
156 | 157 | }); |
| 158 | + |
| 159 | +/** |
| 160 | + * The Object.prototype fall-through pin. Its POPULATION is the point: the |
| 161 | + * suite's other cases iterate the canonical vocabulary only, which is |
| 162 | + * precisely the population that behaves, and that is why this site sat green |
| 163 | + * while `canonicalizeSqlType('constructor')` returned the `Object` FUNCTION |
| 164 | + * out of a signature that admits only `CanonicalSqlType` string literals. |
| 165 | + * |
| 166 | + * `rawType` is uncontrolled — it arrives from live database introspection — |
| 167 | + * so the population below is the reachable one, not a contrived one. |
| 168 | + */ |
| 169 | +describe('canonicalizeSqlType — Object.prototype fall-through', () => { |
| 170 | + // Fixed at five: the three prototype members a lower-cased key can name or |
| 171 | + // nearly name, the assignment-shaped one, and a plain unknown word that |
| 172 | + // names nothing at all. Four is not four-fifths of this pin. |
| 173 | + const POPULATION = ['constructor', 'toString', 'valueOf', '__proto__', 'nope'] as const; |
| 174 | + // Every member of the declared `SqlDialect` union, so the dialect half of the |
| 175 | + // guarded line is covered for each table it can select — including the two |
| 176 | + // the union declares but `DIALECT_ALIASES` does not populate, which fall |
| 177 | + // through to the base map and must answer just as safely. |
| 178 | + const DIALECTS: readonly SqlDialect[] = [ |
| 179 | + 'postgres', 'mysql', 'sqlite', 'snowflake', 'bigquery', 'mongo', |
| 180 | + ]; |
| 181 | + |
| 182 | + // The declared return type is a private union, so the pin names it here. |
| 183 | + // ⛔ This list pins the SIGNATURE, not today's answers: a member added to |
| 184 | + // `CanonicalSqlType` belongs here too. |
| 185 | + const CANONICAL: readonly string[] = [ |
| 186 | + 'text', 'integer', 'bigint', 'decimal', 'float', 'boolean', 'date', 'time', |
| 187 | + 'datetime', 'json', 'uuid', 'binary', 'enum', 'array', 'vector', 'unknown', |
| 188 | + ]; |
| 189 | + |
| 190 | + it.each(POPULATION)('%s resolves to a declared CanonicalSqlType, never a prototype member', (word) => { |
| 191 | + // The assertion is on the SHAPE of the answer, not on which word it is: |
| 192 | + // what the defect produced was a `function`, and pinning "is a declared |
| 193 | + // member of the union" survives a vocabulary change that pinning the |
| 194 | + // string `'unknown'` would break. |
| 195 | + expect(typeof canonicalizeSqlType(word)).toBe('string'); |
| 196 | + expect(CANONICAL).toContain(canonicalizeSqlType(word)); |
| 197 | + for (const dialect of DIALECTS) { |
| 198 | + expect(typeof canonicalizeSqlType(word, dialect)).toBe('string'); |
| 199 | + expect(CANONICAL).toContain(canonicalizeSqlType(word, dialect)); |
| 200 | + } |
| 201 | + }); |
| 202 | + |
| 203 | + it('`constructor` is refused with this function\'s own declared refusal value', () => { |
| 204 | + // `'unknown'` is the trailing `return` of the function itself, ⛔ not a |
| 205 | + // value invented for the fix. |
| 206 | + expect(canonicalizeSqlType('constructor')).toBe('unknown'); |
| 207 | + for (const dialect of DIALECTS) expect(canonicalizeSqlType('constructor', dialect)).toBe('unknown'); |
| 208 | + }); |
| 209 | + |
| 210 | + it('`__proto__` answers `array` from the array-notation rule, ahead of either table', () => { |
| 211 | + // Not a fall-through: `__proto__` starts with `_`, which is Postgres array |
| 212 | + // notation (`_int4`), and that branch returns before any lookup. Recorded |
| 213 | + // so a later reader does not mistake a legitimate declared answer for the |
| 214 | + // defect, and so the array rule cannot be quietly dropped. |
| 215 | + expect(canonicalizeSqlType('__proto__')).toBe('array'); |
| 216 | + expect(canonicalizeSqlType('_int4')).toBe('array'); |
| 217 | + }); |
| 218 | + |
| 219 | + it('the published sibling accessors stay total over the same population', () => { |
| 220 | + // The defect was not confined to this function's own return: a |
| 221 | + // non-`CanonicalSqlType` reaches `CANONICAL_TO_FIELD[canonical]`, which is |
| 222 | + // `undefined`, and both accessors below threw a TypeError on the member |
| 223 | + // read. That is the consequence a plain-JS caller actually meets. |
| 224 | + for (const word of POPULATION) { |
| 225 | + expect(() => suggestFieldTypeForSqlType(word)).not.toThrow(); |
| 226 | + expect(() => isCompatible(word, 'text')).not.toThrow(); |
| 227 | + for (const dialect of DIALECTS) { |
| 228 | + expect(() => suggestFieldTypeForSqlType(word, dialect)).not.toThrow(); |
| 229 | + expect(() => isCompatible(word, 'text', dialect)).not.toThrow(); |
| 230 | + } |
| 231 | + } |
| 232 | + }); |
| 233 | + |
| 234 | + it('lit control — the canonical vocabulary is untouched by the guard', () => { |
| 235 | + // If the guard narrowed anything it should not, these go red. Both halves |
| 236 | + // of the guarded line are represented: the base map and a dialect map. |
| 237 | + expect(canonicalizeSqlType('varchar')).toBe('text'); |
| 238 | + expect(canonicalizeSqlType('numeric(10,2)')).toBe('decimal'); |
| 239 | + expect(canonicalizeSqlType('timestamptz', 'postgres')).toBe('datetime'); |
| 240 | + expect(canonicalizeSqlType('objectid', 'mongo')).toBe('text'); |
| 241 | + }); |
| 242 | +}); |
0 commit comments