Skip to content

Commit a4daa85

Browse files
feat(spec): a plugin-driver datasource can declare its row cap, external.rowCap, up to 10,000 (#29854)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ83WAcW3kdc2RHYZ2aeB5krB)_ Part of #29762 — the spec slice. The engine floor reading the key, and the guide on whole-set views, are the card's second PR, opened after this one merges. Clause-②: yes (widening) — one optional, bounded key joins the external datasource settings; the default keeps today's 1,000 Metadata-protocol: added Skills-docs: follow-up #29762 — this card's second PR teaches the key in content/docs/data-modeling/virtual-objects.mdx and odata-datasources.mdx ## 维护者速读 - 改了什么:外部数据源设置新增可选键 `external.rowCap`(1–10,000,不写即 1,000),只在 spec 层声明与校验。 - 为什么改:客户的虚拟对象甘特图约 5,000 行,固定 1,000 行上限直接 422;Warren 裁定 (a)+(c),平台最大值 10,000。 - 风险与回滚:纯增量键,默认值不变,现有数据源零变化;引擎尚未读取(第二个 PR 才接上),回滚即 revert 本 PR。 - 先看:`packages/spec/src/data/datasource.zod.ts`(键、两个常量、内置驱动拒收)→ `packages/spec/liveness/datasource.json`(planned 行)→ `datasource.test.ts`。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/data/datasource.test.ts -t 29762`,预期 7 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes - `ExternalDatasourceSettingsSchema` declares `rowCap`: `z.number().int().min(1).max(EXTERNAL_ROW_CAP_CEILING).optional()`, beside `queryTimeoutMs`. No default is stamped onto a parsed block, so a served or round-tripped datasource is byte-identical to today's; absent means `EXTERNAL_ROW_CAP_DEFAULT`. - Two constants exported from `@objectstack/spec/data`: `EXTERNAL_ROW_CAP_DEFAULT = 1000` (today's fixed value, which `@objectstack/objectql`'s `EXTERNAL_ROW_CAP` will read from in PR 2) and `EXTERNAL_ROW_CAP_CEILING = 10_000` (the ruled platform maximum). - Refusals, each where the key would do nothing: on a built-in driver at `external.rowCap`, naming the driver (it runs the whole query itself and is never floored); on a managed datasource through the existing managed-block refusal, which lists `rowCap` among the keys to remove. Above the ceiling, zero, negative and fractional values are refused at parse. `maxRows` / `rowLimit` / `maxRowCount` fold onto `rowCap` in the suggestion. - Liveness: `external.rowCap` graded `planned`, with the reader (`external-driver-floor.ts`) named and the flip to `live` owed by PR 2. - Regenerated: `content/docs/references/data/datasource.mdx` (`gen:docs`) and `packages/spec/authorable-surface/data.json` (`gen:schema`). `authorable-surface.base.json` is not touched. Precedent for the shape: `LoopConfigSchema.maxIterations` is bounded by `LOOP_MAX_ITERATIONS_CEILING` the same way (`packages/spec/src/automation/control-flow.zod.ts`). Name chosen: `rowCap`, the customer's own spelling and the `EXTERNAL_ROW_CAP*` family's. ## Tests `packages/spec/src/data/datasource.test.ts`, describe `[#29762] external.rowCap`: the ruled numbers; accepted on a plugin driver from 1 to the ceiling in `external` and `validate-only`; no default stamped on parse; refused above the ceiling, at 0, -1 and 2500.5; the alias fold; refused on `sqlite` naming the driver; refused on a managed datasource through the managed-block refusal. ## Acceptance notes - Measurement before building: issue comment 6595439927. Ruling: (a) + (c), ceiling 10,000; (b) stays with #27261; the 2,000-row renderer ceiling (objectui#7210 ruling a′) was not ruled on. - The engine contract `EngineDatasourceDef.external` gains `rowCap` in PR 2, with the engine that reads it, so this PR adds no surface ahead of a reader beyond the authored key itself. ## Verification All readings on head `d7048915`. - `pnpm --filter @objectstack/spec exec vitest run src/data/datasource.test.ts -t 29762`: 7 passed. Whole file: 69 passed. - `pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/spec test` under the verify lock: `VERDICT command-exit 0`, Test Files 806 passed, Tests 23981 passed, 1 todo. - `pnpm lint` (`eslint . --no-inline-config`): no problems printed. - `node scripts/pm/dispatch-gates.mjs --commands` derived 103 families; all 103 run. `--ran` reconciliation: "103 derived famil(ies) accounted for — 101 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)". <details><summary>Gate readings</summary> - First pass red on three generated artifacts, as expected for two new exports and one new liveness row: `check:api-surface`, `check:export-origins`, `check:generated` and `check:liveness` (the `state-counts/datasource.md` count). Regenerated with `gen:api-surface`, `gen:export-origins`, `gen:liveness-counts` from a freshly built `packages/spec` (commit `d7048915`); all four re-run green. - `check:skill-examples` first refused with exit 3 (client-react not built); after building `@objectstack/client-react...` it re-ran green. - NOT MEASURED, exit 3 (prerequisite): `node scripts/check-plugin-teardown-shape.mjs --self-test` (its positive control is pinned to a commit outside this shallow clone; checker health, not a verdict on this diff) and `pnpm check:dual-build-cjs-loads` (needs every package built). Both run in CI. - Every other derived family: exit 0. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbZdyXCxmKHk7PbwmfZ --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbZdyXCxmKHk7PbwmfZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 55dca46 commit a4daa85

9 files changed

Lines changed: 142 additions & 1 deletion

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a plugin-driver datasource can declare its row cap, `external.rowCap`, up to a platform maximum of 10,000 (#29762)
6+
7+
Clause-②: yes (widening) — one optional, bounded key joins the external datasource settings; the default keeps today's 1,000
8+
Metadata-protocol: added
9+
10+
`ExternalDatasourceSettingsSchema` declares one optional key, `rowCap`: a whole number from 1 to `EXTERNAL_ROW_CAP_CEILING` (10,000). It is the most rows the engine reads from a plugin driver for one request, and so the most rows one read on such a datasource may match before it is refused with `EXTERNAL_ROW_CAP_EXCEEDED` (422). Absent, the cap is `EXTERNAL_ROW_CAP_DEFAULT` (1,000), today's fixed value, so no existing datasource changes. Both constants are exported from `@objectstack/spec/data`.
11+
12+
- **Refused where it would do nothing.** On a built-in driver (which runs the whole query itself and is never capped) the key is refused at `external.rowCap`, naming the driver. On a managed datasource it is refused by the existing managed-block refusal, which names it among the keys to remove. A value above the ceiling, zero, a negative or a fraction is refused at parse. `maxRows` and `rowLimit` are folded onto `rowCap` in the refusal's suggestion.
13+
- **Not acted on yet.** This is the contract half. The engine's floor over a plugin driver reads the key in a following change; until then a datasource that sets it still reads at 1,000 rows. The liveness ledger grades it `planned` until that reader lands.

‎content/docs/references/data/datasource.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const result = DatasourceSchema.parse(data);
7777
| **credentialsRef** | `string` | optional | Reference into the secrets store; never inline credentials. Valid in every schemaMode — the one `external` key a managed datasource may carry. |
7878
| **connector** | `string` | optional | Name of a declared connector instance (a `connectors` entry with a `provider`) whose credential this API-backed datasource borrows — whichever kind the instance's `auth` declares (`bearer`, `api-key` or `basic`; ADR-0143 D2 adds the grant-bearing kinds), resolved the way the connector resolves it. One Connect serves both the connector's actions and this datasource's records. Exclusive with `credentialsRef`; refused on a built-in database driver and on a managed datasource. |
7979
| **queryTimeoutMs** | `number` | optional (default: `30000`) | Hard cap on per-query execution time. |
80+
| **rowCap** | `integer` | optional | Plugin-driver datasources only (a non-managed datasource whose `driver` is not a built-in id): the most rows the engine reads from the driver for one request, and so the most rows one read may match before it is refused with EXTERNAL_ROW_CAP_EXCEEDED (422). Absent: 1000. At most 10000. Each list page reads every match up to this cap from the remote, twice (the rows and the total), so raise it only as far as the widest read needs. |
8081

8182

8283
---
@@ -119,6 +120,7 @@ External datasource settings: federation policy (schemaMode != "managed") plus t
119120
| **credentialsRef** | `string` | optional | Reference into the secrets store; never inline credentials. Valid in every schemaMode — the one `external` key a managed datasource may carry. |
120121
| **connector** | `string` | optional | Name of a declared connector instance (a `connectors` entry with a `provider`) whose credential this API-backed datasource borrows — whichever kind the instance's `auth` declares (`bearer`, `api-key` or `basic`; ADR-0143 D2 adds the grant-bearing kinds), resolved the way the connector resolves it. One Connect serves both the connector's actions and this datasource's records. Exclusive with `credentialsRef`; refused on a built-in database driver and on a managed datasource. |
121122
| **queryTimeoutMs** | `number` | optional (default: `30000`) | Hard cap on per-query execution time. |
123+
| **rowCap** | `integer` | optional | Plugin-driver datasources only (a non-managed datasource whose `driver` is not a built-in id): the most rows the engine reads from the driver for one request, and so the most rows one read may match before it is refused with EXTERNAL_ROW_CAP_EXCEEDED (422). Absent: 1000. At most 10000. Each list page reads every match up to this cap from the remote, twice (the rows and the total), so raise it only as far as the widest read needs. |
122124

123125
### Nested Shape: `ExternalDatasourceSettings.validation`
124126

‎packages/spec/api-surface/data.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@
241241
"EMPTY_OPERATOR_ARMS (const)",
242242
"ENGINE_UPDATE_UPSERT_REMOVED (const)",
243243
"EXTENSION_FIELD_SUFFIX (const)",
244+
"EXTERNAL_ROW_CAP_CEILING (const)",
245+
"EXTERNAL_ROW_CAP_DEFAULT (const)",
244246
"EffectiveApiMethods (interface)",
245247
"EmptyOperatorArm (type)",
246248
"EmptyOperatorExpansion (interface)",

‎packages/spec/authorable-surface/data.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@
275275
"data/ExternalDatasourceSettings:connector",
276276
"data/ExternalDatasourceSettings:credentialsRef",
277277
"data/ExternalDatasourceSettings:queryTimeoutMs",
278+
"data/ExternalDatasourceSettings:rowCap",
278279
"data/ExternalDatasourceSettings:validation",
279280
"data/ExternalTable:columns",
280281
"data/ExternalTable:indexes",

‎packages/spec/export-origins/data.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@
236236
"EMPTY_OPERATOR_ARMS": "src/data/filter-empty-operator.ts#EMPTY_OPERATOR_ARMS (const)",
237237
"ENGINE_UPDATE_UPSERT_REMOVED": "src/data/data-engine.zod.ts#ENGINE_UPDATE_UPSERT_REMOVED (const)",
238238
"EXTENSION_FIELD_SUFFIX": "src/data/field-customization.ts#EXTENSION_FIELD_SUFFIX (const)",
239+
"EXTERNAL_ROW_CAP_CEILING": "src/data/datasource.zod.ts#EXTERNAL_ROW_CAP_CEILING (const)",
240+
"EXTERNAL_ROW_CAP_DEFAULT": "src/data/datasource.zod.ts#EXTERNAL_ROW_CAP_DEFAULT (const)",
239241
"EffectiveApiMethods": "src/data/api-derivation.ts#EffectiveApiMethods (interface)",
240242
"EmptyOperatorArm": "src/data/filter-empty-operator.ts#EmptyOperatorArm (type)",
241243
"EmptyOperatorExpansion": "src/data/filter-empty-operator.ts#EmptyOperatorExpansion (interface)",

‎packages/spec/liveness/datasource.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@
155155
"verifiedAt": "2026-08-28",
156156
"evidence": "packages/services/service-datasource/src/datasource-admin-service.ts#testConnection (`const queryTimeoutMs = (input.external as …)?.queryTimeoutMs`, spread onto the probe options as `timeoutMs` only when it is a number)",
157157
"note": "carried into the external-datasource probe options as `timeoutMs`. 2026-08-28: RE-ANCHORED (commit 9ee2dcfbd) and REPOINTED — `:220` had rotted onto a BLANK LINE, which is the most legible form of this rot: in range, resolvable, and pointing at nothing at all. Re-closed by hand against c459da6bc; DATED for the first time."
158+
},
159+
"rowCap": {
160+
"status": "planned",
161+
"verifiedAt": "2026-11-03",
162+
"note": "PLANNED (#29762, maintainer ruling: a per-datasource cap bounded by a platform maximum of 10,000). The plugin-driver row cap: the most rows the engine's correctness floor reads from a plugin driver for one request, and so the most one read may match before EXTERNAL_ROW_CAP_EXCEEDED (422). Declared in the spec slice (packages/spec/src/data/datasource.zod.ts, beside `EXTERNAL_ROW_CAP_DEFAULT` and `EXTERNAL_ROW_CAP_CEILING`); refused at parse above the ceiling, on a built-in driver, and (through the managed-block refusal) on a managed datasource. ACTED ON by nothing yet: the reader is the floor in packages/objectql/src/external-driver-floor.ts, which lands in the card's second PR. Flip to `live` on the commit that lands that reader, citing it."
158163
}
159164
},
160165
"note": "5 of 7 live. The two dead ones are opposite in risk: `external.label` is cosmetic, `external.requirePermission` is security-shaped — it reads as an access gate and is not one."

‎packages/spec/liveness/state-counts/datasource.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them.
1212

1313
| Type | live | exp | elsewhere | dead | planned | classified |
1414
|---|---|---|---|---|---|---|
15-
| `datasource` | 31 | 0 | 0 | 0 | 0 | 31 |
15+
| `datasource` | 31 | 0 | 0 | 0 | 1 | 32 |

‎packages/spec/src/data/datasource.test.ts‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
DriverType,
66
SchemaModeSchema,
77
ExternalDatasourceSettingsSchema,
8+
EXTERNAL_ROW_CAP_CEILING,
9+
EXTERNAL_ROW_CAP_DEFAULT,
810
type Datasource,
911
} from './datasource.zod';
1012

@@ -958,3 +960,63 @@ describe('external.connector — an API-backed datasource borrows a connector in
958960
expect(result.success, JSON.stringify(result.error?.issues)).toBe(true);
959961
});
960962
});
963+
964+
describe('[#29762] external.rowCap — a plugin-driver datasource\'s row cap, bounded by the platform maximum', () => {
965+
const plugin = { name: 'plan', driver: 'production_plan', config: {}, schemaMode: 'external' } as const;
966+
967+
const issuesAt = (raw: unknown, path: string) => {
968+
const result = DatasourceSchema.safeParse(raw);
969+
return result.success ? [] : result.error.issues.filter((i) => i.path.join('.') === path);
970+
};
971+
972+
it('the default and the ceiling are the ruled numbers', () => {
973+
expect(EXTERNAL_ROW_CAP_DEFAULT).toBe(1000);
974+
expect(EXTERNAL_ROW_CAP_CEILING).toBe(10_000);
975+
});
976+
977+
it('accepts a whole number from 1 to the ceiling on a plugin driver, in external and validate-only modes', () => {
978+
for (const schemaMode of ['external', 'validate-only'] as const) {
979+
for (const rowCap of [1, 5000, EXTERNAL_ROW_CAP_CEILING]) {
980+
const result = DatasourceSchema.safeParse({ ...plugin, schemaMode, external: { rowCap } });
981+
expect(result.success, JSON.stringify(result.error?.issues)).toBe(true);
982+
if (result.success) expect(result.data.external?.rowCap).toBe(rowCap);
983+
}
984+
}
985+
});
986+
987+
it('is absent unless written — no default is stamped onto a parsed block', () => {
988+
const parsed = ExternalDatasourceSettingsSchema.parse({});
989+
expect(parsed).not.toHaveProperty('rowCap');
990+
});
991+
992+
it('refuses a value above the ceiling, zero, a negative and a fraction', () => {
993+
for (const rowCap of [EXTERNAL_ROW_CAP_CEILING + 1, 0, -1, 2500.5]) {
994+
expect(issuesAt({ ...plugin, external: { rowCap } }, 'external.rowCap'), String(rowCap)).toHaveLength(1);
995+
}
996+
});
997+
998+
it('folds the near-miss spellings onto `rowCap`', () => {
999+
for (const spelling of ['maxRows', 'rowLimit']) {
1000+
const result = DatasourceSchema.safeParse({ ...plugin, external: { [spelling]: 5000 } });
1001+
const messages = result.success ? [] : result.error.issues.map((i) => i.message);
1002+
expect(messages.join('\n'), spelling).toContain(`\`${spelling}\` → \`rowCap\``);
1003+
}
1004+
});
1005+
1006+
it('refuses the key on a built-in driver, which is never capped', () => {
1007+
const hits = issuesAt(
1008+
{ name: 'warehouse', driver: 'sqlite', config: { filename: 'warehouse.db' }, schemaMode: 'external', external: { rowCap: 5000 } },
1009+
'external.rowCap',
1010+
);
1011+
expect(hits).toHaveLength(1);
1012+
expect(hits[0]!.code).toBe('custom');
1013+
expect(hits[0]!.message).toContain("'sqlite' driver is built in");
1014+
});
1015+
1016+
it('refuses the key on a managed datasource, through the managed-block refusal that names it', () => {
1017+
const result = DatasourceSchema.safeParse({ ...plugin, schemaMode: 'managed', external: { rowCap: 5000 } });
1018+
expect(result.success).toBe(false);
1019+
const messages = result.success ? [] : result.error.issues.map((i) => i.message).join('\n');
1020+
expect(messages).toContain('remove: rowCap');
1021+
});
1022+
});

‎packages/spec/src/data/datasource.zod.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,26 @@ export const SchemaModeSchema = z
263263

264264
export type SchemaMode = z.input<typeof SchemaModeSchema>;
265265

266+
/**
267+
* [#29762] How many rows the engine reads from a plugin driver for one request
268+
* when the datasource sets no `external.rowCap`: the engine's correctness floor
269+
* (`external-driver-floor.ts` in `@objectstack/objectql`) re-applies the whole
270+
* query over the rows the driver returns, so a read matching more than this is
271+
* refused (`EXTERNAL_ROW_CAP_EXCEEDED`, 422), never answered from a part.
272+
*/
273+
export const EXTERNAL_ROW_CAP_DEFAULT = 1000;
274+
275+
/**
276+
* [#29762] The platform maximum on `external.rowCap` (maintainer ruling on
277+
* #29762: a per-datasource cap bounded by a platform maximum of 10,000).
278+
* Refused above it at parse, as `LoopConfigSchema.maxIterations` is refused
279+
* above `LOOP_MAX_ITERATIONS_CEILING`. Measured before it was set: the engine's
280+
* own decode and re-application of 10,000 twelve-column rows is about 35 ms and
281+
* 2 MiB per read; the remote's cost (every match, twice per list page: the rows
282+
* and the total) is the operator's to judge below it.
283+
*/
284+
export const EXTERNAL_ROW_CAP_CEILING = 10_000;
285+
266286
/**
267287
* External Datasource Settings (ADR-0015)
268288
*
@@ -301,6 +321,9 @@ export const ExternalDatasourceSettingsSchema = strictObject(
301321
connectorinstance: 'connector',
302322
timeoutms: 'queryTimeoutMs',
303323
querytimeout: 'queryTimeoutMs',
324+
maxrows: 'rowCap',
325+
rowlimit: 'rowCap',
326+
maxrowcount: 'rowCap',
304327
},
305328
guidance: {
306329
label: RETIRED_DATASOURCE_BLOCKS.externalLabel,
@@ -373,6 +396,12 @@ export const ExternalDatasourceSettingsSchema = strictObject(
373396
+ '`credentialsRef`; refused on a built-in database driver and on a managed datasource.'),
374397
queryTimeoutMs: z.number().default(30_000)
375398
.describe('Hard cap on per-query execution time.'),
399+
rowCap: z.number().int().min(1).max(EXTERNAL_ROW_CAP_CEILING).optional()
400+
.describe(`Plugin-driver datasources only (a non-managed datasource whose \`driver\` is not a built-in id): `
401+
+ `the most rows the engine reads from the driver for one request, and so the most rows one read may `
402+
+ `match before it is refused with EXTERNAL_ROW_CAP_EXCEEDED (422). Absent: ${EXTERNAL_ROW_CAP_DEFAULT}. `
403+
+ `At most ${EXTERNAL_ROW_CAP_CEILING}. Each list page reads every match up to this cap from the remote, `
404+
+ 'twice (the rows and the total), so raise it only as far as the widest read needs.'),
376405
})
377406
.describe('External datasource settings: federation policy (schemaMode != "managed") '
378407
+ 'plus the secrets-store credentials reference (valid in every schemaMode) or the connector '
@@ -565,6 +594,19 @@ const CONNECTOR_ON_MANAGED_REFUSED =
565594
+ "`'validate-only'` to warn on drift instead of failing) on this datasource. A managed "
566595
+ "datasource's `external` block may carry only `credentialsRef`.";
567596

597+
/**
598+
* [#29762] `external.rowCap` on a built-in driver. The cap is read only by the
599+
* engine's floor over a plugin driver; a built-in driver executes the whole
600+
* query itself and is never floored, so the key would parse and do nothing.
601+
* A managed datasource needs no refusal of its own: a non-default `rowCap` is
602+
* a federation key, which the managed-block refusal already names.
603+
*/
604+
function rowCapOnBuiltinDriverRefusal(driverId: string): string {
605+
return `\`external.rowCap\` caps the rows the engine reads from a plugin driver, which it holds behind `
606+
+ `its correctness floor; the '${driverId}' driver is built in, runs the whole query itself and is `
607+
+ 'never capped, so the key would do nothing here. Remove `rowCap`.';
608+
}
609+
568610
/**
569611
* Replay a driver-config parse onto the datasource's own issue list (#4410).
570612
*
@@ -936,6 +978,18 @@ export const DatasourceSchema = lazySchema(() => strictObject(
936978
}
937979
}
938980

981+
// [#29762] `external.rowCap` is read only by the plugin-driver floor.
982+
if (ds.external?.rowCap !== undefined) {
983+
const builtinDriver = resolveDriverId(ds.driver);
984+
if (builtinDriver !== undefined) {
985+
ctx.addIssue({
986+
code: 'custom',
987+
path: ['external', 'rowCap'],
988+
message: rowCapOnBuiltinDriverRefusal(builtinDriver),
989+
});
990+
}
991+
}
992+
939993
if (ds.schemaMode !== 'managed' && !ds.external) {
940994
ctx.addIssue({
941995
code: 'custom',

0 commit comments

Comments
 (0)