|
1 | 1 | import { describe, it, expect } from 'vitest'; |
2 | 2 | import type { DriverQuery, IDataDriver } from './data-driver'; |
| 3 | +import type { IntrospectedSchema } from './schema-diff-service'; |
3 | 4 | import type { QueryAST } from '../data/query.zod'; |
4 | 5 | import type { DriverOptions } from '../data/driver.zod'; |
5 | 6 |
|
@@ -270,4 +271,131 @@ describe('IDataDriver', () => { |
270 | 271 | expect(unknownOperator.where).toBeTruthy(); |
271 | 272 | }); |
272 | 273 | }); |
| 274 | + |
| 275 | + // =========================================================================== |
| 276 | + // introspectSchema — the engine-registration road meets the compiler (#11493) |
| 277 | + // =========================================================================== |
| 278 | + // |
| 279 | + // #11381 typed the host-factory road (`DatasourceDriverHandle.introspectSchema`, |
| 280 | + // option C of the #11123 ruling). This block pins the OTHER documented road: a |
| 281 | + // driver implementing `IDataDriver` and handed to `IDataEngine.registerDriver()`. |
| 282 | + // Reverse-verified against the pre-#11493 contract (measured 2026-08-24): with |
| 283 | + // the interface silent about `introspectSchema`, the mis-shapes below compiled |
| 284 | + // GREEN — an extra member rides along unchecked — which is the gap #11493 |
| 285 | + // closes. As with the DriverQuery pins above, every directive here is resolved |
| 286 | + // by tsc: reverting the member makes each `@ts-expect-error` unused, and an |
| 287 | + // unused directive is itself an error, so `pnpm --filter @objectstack/spec |
| 288 | + // typecheck` goes red on regression in either direction. |
| 289 | + |
| 290 | + describe('introspectSchema (#11493)', () => { |
| 291 | + /** The declared return type, read off the CONTRACT rather than re-spelled. */ |
| 292 | + type DriverIntrospection = Awaited<ReturnType<NonNullable<IDataDriver['introspectSchema']>>>; |
| 293 | + |
| 294 | + const base: IDataDriver = { |
| 295 | + name: 'introspecting', |
| 296 | + version: '1.0.0', |
| 297 | + supports: {}, |
| 298 | + connect: async () => {}, |
| 299 | + disconnect: async () => {}, |
| 300 | + checkHealth: async () => true, |
| 301 | + execute: async () => ({}), |
| 302 | + find: async () => [], |
| 303 | + findOne: async () => null, |
| 304 | + create: async () => ({ id: '1' }), |
| 305 | + update: async () => ({ id: '1' }), |
| 306 | + upsert: async () => ({ id: '1' }), |
| 307 | + delete: async () => true, |
| 308 | + count: async () => 0, |
| 309 | + bulkCreate: async () => [], |
| 310 | + bulkUpdate: async () => [], |
| 311 | + bulkDelete: async () => {}, |
| 312 | + beginTransaction: async () => ({}), |
| 313 | + commit: async () => {}, |
| 314 | + rollback: async () => {}, |
| 315 | + syncSchema: async () => {}, |
| 316 | + dropTable: async () => {}, |
| 317 | + }; |
| 318 | + |
| 319 | + it('is optional — a driver without introspection stays conformant', () => { |
| 320 | + // `base` above declares no `introspectSchema` and satisfies `IDataDriver` |
| 321 | + // at its declaration; introspection is a capability, not an obligation. |
| 322 | + expect(base.introspectSchema).toBeUndefined(); |
| 323 | + }); |
| 324 | + |
| 325 | + it('declares exactly the spec introspection shape, not a lookalike', () => { |
| 326 | + // Mutual extends: the member's return IS `IntrospectedSchema` — a revert |
| 327 | + // to `unknown` (or a drift to a private re-spelling) resolves `Exact` to |
| 328 | + // `never` and this line goes red naming the contract. |
| 329 | + type Exact = DriverIntrospection extends IntrospectedSchema |
| 330 | + ? (IntrospectedSchema extends DriverIntrospection ? 'exact' : never) |
| 331 | + : never; |
| 332 | + const exact: Exact = 'exact'; |
| 333 | + expect(exact).toBe('exact'); |
| 334 | + }); |
| 335 | + |
| 336 | + it('accepts the spec shape, and a shape that EXTENDS it (the driver-sql pattern)', () => { |
| 337 | + const conforming: IDataDriver = { |
| 338 | + ...base, |
| 339 | + introspectSchema: async () => ({ |
| 340 | + dialect: 'postgres', |
| 341 | + introspectedAt: '2026-08-24T00:00:00.000Z', |
| 342 | + tables: { |
| 343 | + wh_order: { |
| 344 | + name: 'wh_order', |
| 345 | + columns: [{ name: 'id', type: 'uuid', nullable: false, primaryKey: true }], |
| 346 | + }, |
| 347 | + }, |
| 348 | + }), |
| 349 | + }; |
| 350 | + // Extra facts ride along: driver-sql's table-level `primaryKeys` / |
| 351 | + // `foreignKeys` and per-column `isUnique` / `maxLength` live on declared |
| 352 | + // types that EXTEND the spec contract, and assignability admits them on |
| 353 | + // any non-literal value. What the contract refuses is a wrong spelling |
| 354 | + // of a DECLARED key, never a richer driver. |
| 355 | + const extendedResult = { |
| 356 | + dialect: 'postgres', |
| 357 | + introspectedAt: '2026-08-24T00:00:00.000Z', |
| 358 | + tables: { |
| 359 | + wh_order: { |
| 360 | + name: 'wh_order', |
| 361 | + columns: [{ name: 'id', type: 'uuid', nullable: false, primaryKey: true, isUnique: true, maxLength: 36 }], |
| 362 | + primaryKeys: ['id'], |
| 363 | + foreignKeys: [], |
| 364 | + }, |
| 365 | + }, |
| 366 | + }; |
| 367 | + const extended: IDataDriver = { ...base, introspectSchema: async () => extendedResult }; |
| 368 | + expect(typeof conforming.introspectSchema).toBe('function'); |
| 369 | + expect(typeof extended.introspectSchema).toBe('function'); |
| 370 | + }); |
| 371 | + |
| 372 | + it('refuses the retired isPrimary spelling at the offending field', () => { |
| 373 | + // The defect class this seam actually shipped: primary-key membership |
| 374 | + // spelled `isPrimary`, which no consumer reads — the federated table's |
| 375 | + // records silently could not be located or updated. |
| 376 | + // @ts-expect-error - primary-key membership is spelled `primaryKey`, never `isPrimary` |
| 377 | + const misSpelled: DriverIntrospection = { dialect: 'postgres', introspectedAt: 'now', tables: { t: { name: 't', columns: [{ name: 'id', type: 'uuid', nullable: false, isPrimary: true }] } } }; |
| 378 | + expect(misSpelled).toBeTruthy(); |
| 379 | + }); |
| 380 | + |
| 381 | + it('refuses a bare { tables } with no dialect / introspectedAt envelope', () => { |
| 382 | + // @ts-expect-error - `dialect` and `introspectedAt` are REQUIRED on the spec schema |
| 383 | + const bareTables: DriverIntrospection = { tables: {} }; |
| 384 | + expect(bareTables).toBeTruthy(); |
| 385 | + }); |
| 386 | + |
| 387 | + it('refuses a mis-shaped implementation where it is OFFERED, on the registerDriver road', () => { |
| 388 | + // Exactly what a pre-#11493 driver author shipped: the whole driver value, |
| 389 | + // with an `introspectSchema` answering the pre-spec shape. Against the |
| 390 | + // silent contract this assignment compiled green (the measured gap); |
| 391 | + // declared, tsc refuses it at the member. |
| 392 | + const preFixResult = { tables: { t: { name: 't', columns: [{ name: 'id', type: 'uuid', nullable: false, isPrimary: true }] } } }; |
| 393 | + const author: IDataDriver = { |
| 394 | + ...base, |
| 395 | + // @ts-expect-error - the pre-spec result shape no longer satisfies the declared member |
| 396 | + introspectSchema: async () => preFixResult, |
| 397 | + }; |
| 398 | + expect(author).toBeTruthy(); |
| 399 | + }); |
| 400 | + }); |
273 | 401 | }); |
0 commit comments