Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/27744-plugin-driver-conformance-kit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/objectql': patch
---

feat(objectql): a conformance kit a plugin driver outside this repository runs in its own CI, so a change to the contract the engine holds plugin drivers to is caught there

Clause-②: yes
Metadata-protocol: none

A plugin driver (a `driver.NAME` service a server plugin publishes, such as a vendor's closed-source Salesforce driver) serves an external datasource behind the engine's correctness floor. The floor's contract was pinned only by this package's own tests, which a driver living in another repository never runs. `@objectstack/objectql` now exports the kit that driver runs instead.

- **What it asserts.** The floor's contract, against the spec's driver conformance tables (`FILTER_LOGIC_CASES`, `FILTER_TEXT_CASES`, `TEMPORAL_CASES`, `PAGINATION_CASES`): for every filter, the driver's own answer to the query the floor sends holds every matching row and only stored rows, or refuses with a status of 400, 404 or 422, and the floored answer equals the table's. Also: each row's stored values come back unchanged (a `null` stays `null`, a number stays a number), `count` through the floor, the floor's paged walk by every table `orderBy`, write refusals reaching the caller as `422 EXTERNAL_WRITE_REFUSED` (from write probes the caller names), an outage reaching the caller as `503 ERR_DATASOURCE_UNAVAILABLE` and never as rows (from an unavailable remote the caller builds), and the `introspectSchema()` shape when the driver offers it.
- **Pushdown is measured.** `DriverCapabilities` carries no pushdown claim, so each read case reports how the driver answered: `exact`, `superset` or `refused`.
- **A second tier, `direct`.** Opt-in: the driver answers the whole query itself (an exact filter, `count`, `findOne`, a deterministic paged walk, `limit` by presence). The floor does not need it; it is the oracle a later verified-pushdown credential, letting a tested driver skip the floor, will be built on.
- **How to run it.** Hand `setup(fixture)` a driver over a remote holding exactly the fixture's rows (a fake of the remote's API in front of the driver's real translation code, or a sandbox seeded from `pluginDriverConformanceFixtures()`). Then `definePluginDriverConformanceSuite({ describe, it, afterAll }, options)` registers one test per case under vitest or jest, or `runPluginDriverConformance(options)` answers a report with no test framework. Case ids (`tier/group/fixture/case name`) are stable.

New exports: `createPluginDriverConformance`, `runPluginDriverConformance`, `definePluginDriverConformanceSuite`, `pluginDriverConformanceFixtures`, `PLUGIN_DRIVER_CONFORMANCE_FIXTURE_NAMES`, `PluginDriverConformanceFailure`, and their types. Nothing existing changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* driver-odata through the plugin-driver conformance kit
* (`definePluginDriverConformanceSuite`, `@objectstack/objectql`): the same kit
* an out-of-repo plugin driver runs in its own CI, run here by the first
* in-repo plugin driver, so a kit change that a real driver cannot pass fails
* in this repository first.
*
* Each fixture is served by the in-process OData v4 service
* (`__fixtures__/odata-service.ts`), which evaluates the `$filter` the driver
* writes, over both collations. The floor tier only: the driver pushes down a
* superset when a `where` does not translate whole, which the floor needs and
* the direct tier (a driver that skips the floor) refuses by design.
*/

import { afterAll, describe, it } from 'vitest';
import {
definePluginDriverConformanceSuite,
type PluginDriverConformanceFixture,
type PluginDriverConformanceTarget,
} from '@objectstack/objectql';
import { parseODataDriverConfig } from './config.js';
import { ODataDriver } from './odata-driver.js';
import type { FetchLike } from './transport.js';
import { CONFORMANCE_ROOT, odataService, type EdmType, type ServiceProperty } from './__fixtures__/odata-service.js';

const ENTITY_SET = 'Rows';
const EDM: Record<PluginDriverConformanceFixture['fields'][string]['type'], EdmType> = {
text: 'Edm.String',
number: 'Edm.Double',
datetime: 'Edm.DateTimeOffset',
date: 'Edm.Date',
};

/** The driver over `fetch`, bound to the fixture's object as an external object on the service's one entity set. */
function driverOver(fixture: PluginDriverConformanceFixture, fetch: FetchLike): PluginDriverConformanceTarget {
const driver = new ODataDriver(parseODataDriverConfig({ name: 'conformance', serviceRoot: CONFORMANCE_ROOT }), { fetch });
driver.registerExternalObject({ name: fixture.object, external: { remoteName: ENTITY_SET } });
return { driver };
}

for (const collation of ['ordinal', 'case-insensitive'] as const) {
definePluginDriverConformanceSuite(
{ describe, it, afterAll },
{
setup: (fixture) => {
const properties: ServiceProperty[] = [
{ name: 'id', type: 'Edm.String', nullable: false },
...Object.entries(fixture.fields).map(([name, f]) => ({ name, type: EDM[f.type] })),
];
const service = odataService({ entitySet: ENTITY_SET, properties, rows: fixture.rows, collation, serverPageSize: 5 });
return driverOver(fixture, service.fetch);
},
setupUnavailable: (fixture) => driverOver(fixture, async () => { throw new TypeError('fetch failed: connect ECONNREFUSED'); }),
writeProbes: [
// The service answers 409 to a second entity under an existing key.
{ name: 'a create under a key the service already holds', operation: 'create', args: [{ id: '1', a: 'x' }], expect: 'refused' },
{ name: 'an update by key the service accepts', operation: 'update', args: ['1', { a: 'changed' }], expect: 'accepted' },
],
},
`driver-odata through the plugin-driver conformance kit, over a ${collation} service`,
);
}
26 changes: 26 additions & 0 deletions packages/objectql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,32 @@ export {
} from './external-driver-errors.js';
export type { ExternalWriteOperation } from './external-driver-errors.js';
export { needsExternalDriverFloor, unwrapExternalDriverFloor } from './external-driver-floor.js';
// [#27744] The conformance kit a plugin driver outside this repository runs in
// its own CI: the floor's contract against the spec's driver conformance tables.
export {
createPluginDriverConformance,
runPluginDriverConformance,
definePluginDriverConformanceSuite,
pluginDriverConformanceFixtures,
PLUGIN_DRIVER_CONFORMANCE_FIXTURE_NAMES,
PluginDriverConformanceFailure,
} from './plugin-driver-conformance.js';
export type {
PluginDriverConformance,
PluginDriverConformanceCase,
PluginDriverConformanceField,
PluginDriverConformanceFixture,
PluginDriverConformanceFixtureName,
PluginDriverConformanceGroup,
PluginDriverConformanceOptions,
PluginDriverConformanceOutcome,
PluginDriverConformanceReport,
PluginDriverConformanceTarget,
PluginDriverConformanceTestApi,
PluginDriverConformanceTier,
PluginDriverConformanceWriteProbe,
PluginDriverPushdown,
} from './plugin-driver-conformance.js';
export type { InsertManyRowOutcome } from './engine.js';
// [#5696] Thrown by `transaction(cb, base, { require: true })` when the
// datasource cannot give a real transaction. Exported so a caller that fails
Expand Down
Loading
Loading