@vibeorm/runtime 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +29 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/diagnostics/index.d.ts +3 -1
- package/dist/diagnostics/index.d.ts.map +1 -1
- package/dist/diagnostics/plan.d.ts +36 -0
- package/dist/diagnostics/plan.d.ts.map +1 -1
- package/dist/diagnostics/statement-diagnostics.d.ts +172 -0
- package/dist/diagnostics/statement-diagnostics.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +928 -365
- package/dist/index.js.map +13 -12
- package/dist/nested-writes.d.ts.map +1 -1
- package/dist/query-builder.d.ts +11 -0
- package/dist/query-builder.d.ts.map +1 -1
- package/dist/relation-loader.d.ts +12 -0
- package/dist/relation-loader.d.ts.map +1 -1
- package/dist/relation-plan.d.ts +14 -0
- package/dist/relation-plan.d.ts.map +1 -1
- package/dist/rls-readiness.d.ts.map +1 -1
- package/dist/telemetry/statement.d.ts +7 -0
- package/dist/telemetry/statement.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1649,6 +1649,11 @@ function singleKeyPair(params) {
|
|
|
1649
1649
|
}
|
|
1650
1650
|
return { parent: link.parentKeys[0], child: link.childKeys[0] };
|
|
1651
1651
|
}
|
|
1652
|
+
function inverseRelationName(params) {
|
|
1653
|
+
const { link } = params;
|
|
1654
|
+
const reverse = findReverseRelation({ relation: link.relation, target: link.target });
|
|
1655
|
+
return reverse === null ? null : reverse.name;
|
|
1656
|
+
}
|
|
1652
1657
|
function singlePrimaryKey(params) {
|
|
1653
1658
|
const { model, relation } = params;
|
|
1654
1659
|
const pk = model.primaryKey;
|
|
@@ -3294,21 +3299,27 @@ function expandCompoundWhere(params) {
|
|
|
3294
3299
|
function isAddressableMember(params) {
|
|
3295
3300
|
return isDirectValue(params.value) || params.nullsNotDistinct && params.value === null;
|
|
3296
3301
|
}
|
|
3297
|
-
function
|
|
3298
|
-
const { model, where
|
|
3302
|
+
function isUniqueWhere(params) {
|
|
3303
|
+
const { model, where } = params;
|
|
3299
3304
|
for (const field of model.uniqueFields) {
|
|
3300
3305
|
if (isDirectValue(where[field.name]))
|
|
3301
|
-
return;
|
|
3306
|
+
return true;
|
|
3302
3307
|
}
|
|
3303
3308
|
for (const single of model.singleIndexUniques) {
|
|
3304
3309
|
const field = single.fields[0];
|
|
3305
3310
|
if (isAddressableMember({ value: where[field.name], nullsNotDistinct: single.nullsNotDistinct }))
|
|
3306
|
-
return;
|
|
3311
|
+
return true;
|
|
3307
3312
|
}
|
|
3308
3313
|
for (const group of model.compoundUniques.values()) {
|
|
3309
3314
|
if (group.fields.every((field) => isAddressableMember({ value: where[field.name], nullsNotDistinct: group.nullsNotDistinct })))
|
|
3310
|
-
return;
|
|
3315
|
+
return true;
|
|
3311
3316
|
}
|
|
3317
|
+
return false;
|
|
3318
|
+
}
|
|
3319
|
+
function assertUniqueWhere(params) {
|
|
3320
|
+
const { model, where, method } = params;
|
|
3321
|
+
if (isUniqueWhere({ model, where }))
|
|
3322
|
+
return;
|
|
3312
3323
|
const candidates = [
|
|
3313
3324
|
...model.uniqueFields.map((field) => field.name),
|
|
3314
3325
|
...model.singleIndexUniques.map((single) => single.fields[0].name),
|
|
@@ -5586,21 +5597,41 @@ function joinValues(params) {
|
|
|
5586
5597
|
const childValue = encodeValue({ dialect: dialect.name, field: pair.child, value: childKey });
|
|
5587
5598
|
return join.parentColumn === "A" ? { a: parentValue, b: childValue } : { a: childValue, b: parentValue };
|
|
5588
5599
|
}
|
|
5589
|
-
|
|
5590
|
-
const {
|
|
5600
|
+
function chunkKeyList(params) {
|
|
5601
|
+
const { dialect, keys, perKey, reserved } = params;
|
|
5602
|
+
if (keys.length === 0)
|
|
5603
|
+
return [];
|
|
5604
|
+
if (dialect.inArrayStrategy !== "expand")
|
|
5605
|
+
return [keys];
|
|
5606
|
+
const size = Math.max(1, Math.floor((dialect.paramBudget - reserved) / perKey));
|
|
5607
|
+
const chunks = [];
|
|
5608
|
+
for (let start = 0;start < keys.length; start += size)
|
|
5609
|
+
chunks.push(keys.slice(start, start + size));
|
|
5610
|
+
return chunks;
|
|
5611
|
+
}
|
|
5612
|
+
async function insertJoinRows(params) {
|
|
5613
|
+
const { run, dialect, link, parentKey, childKeys } = params;
|
|
5614
|
+
if (childKeys.length === 0)
|
|
5615
|
+
return;
|
|
5591
5616
|
const join = link.join;
|
|
5592
|
-
const
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
kind: "insert",
|
|
5596
|
-
table: { name: join.tableName },
|
|
5597
|
-
columns: ["A", "B"],
|
|
5598
|
-
rows: [[a, b]],
|
|
5599
|
-
onConflict: { columns: ["A", "B"], doNothing: true }
|
|
5600
|
-
},
|
|
5601
|
-
model: link.relation.model,
|
|
5602
|
-
method: "nested.connect"
|
|
5617
|
+
const rows = childKeys.map((childKey) => {
|
|
5618
|
+
const { a, b } = joinValues({ dialect, link, parentKey, childKey });
|
|
5619
|
+
return [a, b];
|
|
5603
5620
|
});
|
|
5621
|
+
const perChunk = Math.max(1, Math.floor(dialect.paramBudget / 2));
|
|
5622
|
+
for (let start = 0;start < rows.length; start += perChunk) {
|
|
5623
|
+
await run.statement({
|
|
5624
|
+
statement: {
|
|
5625
|
+
kind: "insert",
|
|
5626
|
+
table: { name: join.tableName },
|
|
5627
|
+
columns: ["A", "B"],
|
|
5628
|
+
rows: rows.slice(start, start + perChunk),
|
|
5629
|
+
onConflict: { columns: ["A", "B"], doNothing: true }
|
|
5630
|
+
},
|
|
5631
|
+
model: link.relation.model,
|
|
5632
|
+
method: "nested.connect"
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5604
5635
|
}
|
|
5605
5636
|
async function deleteJoinRows(params) {
|
|
5606
5637
|
const { run, dialect, link, parentKey, childKeys } = params;
|
|
@@ -5629,42 +5660,58 @@ async function deleteJoinRows(params) {
|
|
|
5629
5660
|
method: "nested.disconnect"
|
|
5630
5661
|
});
|
|
5631
5662
|
}
|
|
5632
|
-
async function
|
|
5633
|
-
const { run, ctx, link, parentKey,
|
|
5634
|
-
|
|
5635
|
-
const pair = singleKeyPair({ link, what: "implicit many-to-many writes" });
|
|
5636
|
-
const childValue = encodeValue({ dialect: ctx.dialect.name, field: pair.child, value: childKey });
|
|
5637
|
-
if (members.some((member) => String(member) === String(childValue)))
|
|
5663
|
+
async function assertM2MMembers(params) {
|
|
5664
|
+
const { run, ctx, link, parentKey, childKeys, target, verb, relation } = params;
|
|
5665
|
+
if (childKeys.length === 0)
|
|
5638
5666
|
return;
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5667
|
+
const members = await loadMemberKeys({ run, ctx, link, parentKey, childKeys });
|
|
5668
|
+
const pair = singleKeyPair({ link, what: "implicit many-to-many writes" });
|
|
5669
|
+
const connected = new Set(members.map((member) => String(member)));
|
|
5670
|
+
for (const childKey of childKeys) {
|
|
5671
|
+
const childValue = encodeValue({ dialect: ctx.dialect.name, field: pair.child, value: childKey });
|
|
5672
|
+
if (connected.has(String(childValue)))
|
|
5673
|
+
continue;
|
|
5674
|
+
throw notFound({
|
|
5675
|
+
message: `nested "${verb}" on ${link.relation.model}.${relation}: ${target} is not connected`,
|
|
5676
|
+
meta: { relation }
|
|
5677
|
+
});
|
|
5678
|
+
}
|
|
5643
5679
|
}
|
|
5644
|
-
async function
|
|
5645
|
-
const { run, dialect, link,
|
|
5680
|
+
async function deleteJoinRowsForChildren(params) {
|
|
5681
|
+
const { run, dialect, link, childKeys } = params;
|
|
5682
|
+
if (childKeys.length === 0)
|
|
5683
|
+
return;
|
|
5646
5684
|
const join = link.join;
|
|
5647
|
-
const
|
|
5648
|
-
|
|
5649
|
-
field: singleKeyPair({ link, what: "implicit many-to-many writes" }).child,
|
|
5650
|
-
value: childKey
|
|
5651
|
-
});
|
|
5685
|
+
const pair = singleKeyPair({ link, what: "implicit many-to-many writes" });
|
|
5686
|
+
const values = childKeys.map((childKey) => encodeValue({ dialect: dialect.name, field: pair.child, value: childKey }));
|
|
5652
5687
|
const isSelf = link.relation.model === link.relation.targetModel;
|
|
5653
|
-
|
|
5654
|
-
statement
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5688
|
+
for (const chunk of chunkKeyList({ dialect, keys: values, perKey: isSelf ? 2 : 1, reserved: 0 })) {
|
|
5689
|
+
await run.statement({
|
|
5690
|
+
statement: {
|
|
5691
|
+
kind: "delete",
|
|
5692
|
+
table: { name: join.tableName },
|
|
5693
|
+
where: isSelf ? {
|
|
5694
|
+
kind: "or",
|
|
5695
|
+
operands: [
|
|
5696
|
+
{ kind: "inArray", column: { name: "A" }, values: [...chunk] },
|
|
5697
|
+
{ kind: "inArray", column: { name: "B" }, values: [...chunk] }
|
|
5698
|
+
]
|
|
5699
|
+
} : { kind: "inArray", column: { name: join.childColumn }, values: [...chunk] }
|
|
5700
|
+
},
|
|
5701
|
+
model: link.relation.model,
|
|
5702
|
+
method: "nested.delete"
|
|
5703
|
+
});
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
async function deleteChildrenByKey(params) {
|
|
5707
|
+
const { run, dialect, link, childKeys } = params;
|
|
5708
|
+
for (const chunk of chunkKeyList({ dialect, keys: childKeys, perKey: 1, reserved: 0 })) {
|
|
5709
|
+
await run.affected({
|
|
5710
|
+
model: link.target.name,
|
|
5711
|
+
method: "deleteMany",
|
|
5712
|
+
args: { where: childAssignment({ link, values: [{ in: [...chunk] }] }) }
|
|
5713
|
+
});
|
|
5714
|
+
}
|
|
5668
5715
|
}
|
|
5669
5716
|
function singleVerb(params) {
|
|
5670
5717
|
const { op } = params;
|
|
@@ -5703,6 +5750,48 @@ async function resolveOwnerCreateVerb(params) {
|
|
|
5703
5750
|
}
|
|
5704
5751
|
}
|
|
5705
5752
|
}
|
|
5753
|
+
function isFilterableUniqueWhere(params) {
|
|
5754
|
+
const { target, where } = params;
|
|
5755
|
+
const entries = Object.entries(where).filter(([, value]) => value !== undefined);
|
|
5756
|
+
if (entries.length === 0)
|
|
5757
|
+
return false;
|
|
5758
|
+
if (!entries.every(([key, value]) => target.fieldByName.has(key) && isDirectValue2(value)))
|
|
5759
|
+
return false;
|
|
5760
|
+
return isUniqueWhere({ model: target, where });
|
|
5761
|
+
}
|
|
5762
|
+
async function connectChildrenByFk(params) {
|
|
5763
|
+
const { run, op, items, fk, verb } = params;
|
|
5764
|
+
const target = op.link.target;
|
|
5765
|
+
if (items.length === 0)
|
|
5766
|
+
return;
|
|
5767
|
+
const batchable = items.length > 1 && items.every((item) => isFilterableUniqueWhere({ target, where: item }));
|
|
5768
|
+
if (!batchable) {
|
|
5769
|
+
for (const item of items) {
|
|
5770
|
+
await run.single({ model: target.name, method: "update", args: { where: item, data: fk } });
|
|
5771
|
+
}
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
const affected = await run.affected({
|
|
5775
|
+
model: target.name,
|
|
5776
|
+
method: "updateMany",
|
|
5777
|
+
args: { where: { OR: items.map((item) => ({ ...item })) }, data: fk }
|
|
5778
|
+
});
|
|
5779
|
+
if (affected >= items.length)
|
|
5780
|
+
return;
|
|
5781
|
+
for (const item of items) {
|
|
5782
|
+
const found = await run.rows({
|
|
5783
|
+
model: target.name,
|
|
5784
|
+
method: "findUnique",
|
|
5785
|
+
args: { where: item, select: childKeySelect({ link: op.link }) }
|
|
5786
|
+
});
|
|
5787
|
+
if (found[0] !== undefined)
|
|
5788
|
+
continue;
|
|
5789
|
+
throw notFound({
|
|
5790
|
+
message: `no ${target.name} record matched the where clause of update()`,
|
|
5791
|
+
meta: { model: target.name, method: "update", relation: op.name, verb }
|
|
5792
|
+
});
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5706
5795
|
function sameDefinedKeys(rows) {
|
|
5707
5796
|
const first = rows[0];
|
|
5708
5797
|
if (first === undefined)
|
|
@@ -5731,14 +5820,14 @@ async function applyChildCreateOps(params) {
|
|
|
5731
5820
|
continue;
|
|
5732
5821
|
}
|
|
5733
5822
|
}
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5823
|
+
if (link.kind === "manyToMany") {
|
|
5824
|
+
const childKeys = [];
|
|
5825
|
+
for (const item of items) {
|
|
5737
5826
|
if (verb === "connect") {
|
|
5738
|
-
|
|
5827
|
+
childKeys.push((await resolveTargetKeys({ run, link, where: item, what: `connect ${op.name}` }))[0]);
|
|
5739
5828
|
} else if (verb === "create") {
|
|
5740
5829
|
const child = await runCreate({ ctx, run, model: target, data: item });
|
|
5741
|
-
|
|
5830
|
+
childKeys.push(childTupleOf({ link, row: child })[0]);
|
|
5742
5831
|
} else {
|
|
5743
5832
|
requireShape({ op, verb, item, keys: ["where", "create"] });
|
|
5744
5833
|
const found = await run.rows({
|
|
@@ -5746,18 +5835,22 @@ async function applyChildCreateOps(params) {
|
|
|
5746
5835
|
method: "findUnique",
|
|
5747
5836
|
args: { where: item.where, select: childKeySelect({ link }) }
|
|
5748
5837
|
});
|
|
5749
|
-
|
|
5838
|
+
childKeys.push(found[0] !== undefined ? childTupleOf({ link, row: found[0] })[0] : childTupleOf({
|
|
5750
5839
|
link,
|
|
5751
5840
|
row: await runCreate({ ctx, run, model: target, data: item.create })
|
|
5752
|
-
})[0];
|
|
5841
|
+
})[0]);
|
|
5753
5842
|
}
|
|
5754
|
-
await insertJoinRow({ run, dialect: ctx.dialect, link, parentKey, childKey });
|
|
5755
|
-
continue;
|
|
5756
5843
|
}
|
|
5844
|
+
await insertJoinRows({ run, dialect: ctx.dialect, link, parentKey, childKeys });
|
|
5845
|
+
continue;
|
|
5846
|
+
}
|
|
5847
|
+
if (verb === "connect") {
|
|
5848
|
+
await connectChildrenByFk({ run, op, items, fk, verb });
|
|
5849
|
+
continue;
|
|
5850
|
+
}
|
|
5851
|
+
for (const item of items) {
|
|
5757
5852
|
if (verb === "create") {
|
|
5758
5853
|
await runCreate({ ctx, run, model: target, data: { ...item, ...fk } });
|
|
5759
|
-
} else if (verb === "connect") {
|
|
5760
|
-
await run.single({ model: target.name, method: "update", args: { where: item, data: fk } });
|
|
5761
5854
|
} else {
|
|
5762
5855
|
requireShape({ op, verb, item, keys: ["where", "create"] });
|
|
5763
5856
|
const found = await run.rows({
|
|
@@ -5956,28 +6049,52 @@ async function resolveOwnerUpdateVerb(params) {
|
|
|
5956
6049
|
}
|
|
5957
6050
|
}
|
|
5958
6051
|
async function loadMemberKeys(params) {
|
|
5959
|
-
const { run, ctx, link, parentKey } = params;
|
|
6052
|
+
const { run, ctx, link, parentKey, childKeys } = params;
|
|
5960
6053
|
const join = link.join;
|
|
5961
|
-
const
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
6054
|
+
const pair = singleKeyPair({ link, what: "implicit many-to-many writes" });
|
|
6055
|
+
const parentMatch = {
|
|
6056
|
+
kind: "cmp",
|
|
6057
|
+
op: "eq",
|
|
6058
|
+
column: { name: join.parentColumn },
|
|
6059
|
+
value: encodeValue({ dialect: ctx.dialect.name, field: pair.parent, value: parentKey })
|
|
6060
|
+
};
|
|
6061
|
+
const scan = async (narrow) => {
|
|
6062
|
+
const rows = await run.statement({
|
|
6063
|
+
statement: {
|
|
6064
|
+
kind: "select",
|
|
6065
|
+
table: { name: join.tableName },
|
|
6066
|
+
columns: [{ name: join.childColumn }],
|
|
6067
|
+
where: narrow === null ? parentMatch : {
|
|
6068
|
+
kind: "and",
|
|
6069
|
+
operands: [
|
|
6070
|
+
parentMatch,
|
|
6071
|
+
{ kind: "inArray", column: { name: join.childColumn }, values: [...narrow] }
|
|
6072
|
+
]
|
|
6073
|
+
}
|
|
6074
|
+
},
|
|
6075
|
+
model: link.relation.model,
|
|
6076
|
+
method: "nested.members"
|
|
6077
|
+
});
|
|
6078
|
+
return rows.map((row) => row[join.childColumn]);
|
|
6079
|
+
};
|
|
6080
|
+
if (childKeys === undefined)
|
|
6081
|
+
return await scan(null);
|
|
6082
|
+
const values = childKeys.map((childKey) => encodeValue({ dialect: ctx.dialect.name, field: pair.child, value: childKey }));
|
|
6083
|
+
const found = [];
|
|
6084
|
+
for (const chunk of chunkKeyList({ dialect: ctx.dialect, keys: values, perKey: 1, reserved: 1 })) {
|
|
6085
|
+
found.push(...await scan(chunk));
|
|
6086
|
+
}
|
|
6087
|
+
return found;
|
|
6088
|
+
}
|
|
6089
|
+
async function membershipFilter(params) {
|
|
6090
|
+
const { run, ctx, link, parentKey } = params;
|
|
6091
|
+
const inverse = inverseRelationName({ link });
|
|
6092
|
+
if (inverse !== null) {
|
|
6093
|
+
const pair = singleKeyPair({ link, what: "implicit many-to-many writes" });
|
|
6094
|
+
return { [inverse]: { some: { [pair.parent.name]: parentKey } } };
|
|
6095
|
+
}
|
|
6096
|
+
const members = await loadMemberKeys({ run, ctx, link, parentKey });
|
|
6097
|
+
return childAssignment({ link, values: [{ in: members }] });
|
|
5981
6098
|
}
|
|
5982
6099
|
async function applyChildUpdateOps(params) {
|
|
5983
6100
|
const { ctx, run, op, parentValues } = params;
|
|
@@ -5999,10 +6116,11 @@ async function applyChildUpdateOps(params) {
|
|
|
5999
6116
|
const items = toItems({ op, verb, operand: operand ?? [] });
|
|
6000
6117
|
if (isM2M) {
|
|
6001
6118
|
await deleteJoinRows({ run, dialect: ctx.dialect, link, parentKey });
|
|
6119
|
+
const childKeys = [];
|
|
6002
6120
|
for (const item of items) {
|
|
6003
|
-
|
|
6004
|
-
await insertJoinRow({ run, dialect: ctx.dialect, link, parentKey, childKey });
|
|
6121
|
+
childKeys.push((await resolveTargetKeys({ run, link, where: item, what: `set ${op.name}` }))[0]);
|
|
6005
6122
|
}
|
|
6123
|
+
await insertJoinRows({ run, dialect: ctx.dialect, link, parentKey, childKeys });
|
|
6006
6124
|
} else {
|
|
6007
6125
|
const detach = childDetach({ op, verb });
|
|
6008
6126
|
await run.affected({
|
|
@@ -6010,9 +6128,7 @@ async function applyChildUpdateOps(params) {
|
|
|
6010
6128
|
method: "updateMany",
|
|
6011
6129
|
args: { where: fk, data: detach }
|
|
6012
6130
|
});
|
|
6013
|
-
|
|
6014
|
-
await run.single({ model: target.name, method: "update", args: { where: item, data: fk } });
|
|
6015
|
-
}
|
|
6131
|
+
await connectChildrenByFk({ run, op, items, fk, verb });
|
|
6016
6132
|
}
|
|
6017
6133
|
break;
|
|
6018
6134
|
}
|
|
@@ -6054,23 +6170,31 @@ async function applyChildUpdateOps(params) {
|
|
|
6054
6170
|
break;
|
|
6055
6171
|
}
|
|
6056
6172
|
const items = toItems({ op, verb, operand });
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
await
|
|
6061
|
-
await deleteJoinRowsForChild({ run, dialect: ctx.dialect, link, childKey });
|
|
6062
|
-
await run.single({
|
|
6063
|
-
model: target.name,
|
|
6064
|
-
method: "delete",
|
|
6065
|
-
args: { where: childAssignment({ link, values: [childKey] }) }
|
|
6066
|
-
});
|
|
6067
|
-
} else {
|
|
6068
|
-
await run.affected({
|
|
6069
|
-
model: target.name,
|
|
6070
|
-
method: "deleteMany",
|
|
6071
|
-
args: { where: { AND: [item, fk] } }
|
|
6072
|
-
});
|
|
6173
|
+
if (isM2M) {
|
|
6174
|
+
const childKeys = [];
|
|
6175
|
+
for (const item of items) {
|
|
6176
|
+
childKeys.push((await resolveTargetKeys({ run, link, where: item, what: `delete ${op.name}` }))[0]);
|
|
6073
6177
|
}
|
|
6178
|
+
await assertM2MMembers({
|
|
6179
|
+
run,
|
|
6180
|
+
ctx,
|
|
6181
|
+
link,
|
|
6182
|
+
parentKey,
|
|
6183
|
+
childKeys,
|
|
6184
|
+
target: target.name,
|
|
6185
|
+
verb: "delete",
|
|
6186
|
+
relation: op.name
|
|
6187
|
+
});
|
|
6188
|
+
await deleteJoinRowsForChildren({ run, dialect: ctx.dialect, link, childKeys });
|
|
6189
|
+
await deleteChildrenByKey({ run, dialect: ctx.dialect, link, childKeys });
|
|
6190
|
+
break;
|
|
6191
|
+
}
|
|
6192
|
+
for (const item of items) {
|
|
6193
|
+
await run.affected({
|
|
6194
|
+
model: target.name,
|
|
6195
|
+
method: "deleteMany",
|
|
6196
|
+
args: { where: { AND: [item, fk] } }
|
|
6197
|
+
});
|
|
6074
6198
|
}
|
|
6075
6199
|
break;
|
|
6076
6200
|
}
|
|
@@ -6096,41 +6220,44 @@ async function applyChildUpdateOps(params) {
|
|
|
6096
6220
|
break;
|
|
6097
6221
|
}
|
|
6098
6222
|
const items = toItems({ op, verb, operand });
|
|
6099
|
-
for (const item of items)
|
|
6223
|
+
for (const item of items)
|
|
6100
6224
|
requireShape({ op, verb, item, keys: ["where", "data"] });
|
|
6101
|
-
|
|
6102
|
-
|
|
6225
|
+
if (isM2M) {
|
|
6226
|
+
const childKeys = [];
|
|
6227
|
+
for (const item of items) {
|
|
6228
|
+
childKeys.push((await resolveTargetKeys({
|
|
6103
6229
|
run,
|
|
6104
6230
|
link,
|
|
6105
6231
|
where: item.where,
|
|
6106
6232
|
what: `update ${op.name}`
|
|
6107
|
-
}))[0];
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
}
|
|
6233
|
+
}))[0]);
|
|
6234
|
+
}
|
|
6235
|
+
await assertM2MMembers({
|
|
6236
|
+
run,
|
|
6237
|
+
ctx,
|
|
6238
|
+
link,
|
|
6239
|
+
parentKey,
|
|
6240
|
+
childKeys,
|
|
6241
|
+
target: target.name,
|
|
6242
|
+
verb: "update",
|
|
6243
|
+
relation: op.name
|
|
6244
|
+
});
|
|
6245
|
+
for (const item of items) {
|
|
6121
6246
|
await run.single({ model: target.name, method: "update", args: { where: item.where, data: item.data } });
|
|
6122
|
-
}
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6247
|
+
}
|
|
6248
|
+
break;
|
|
6249
|
+
}
|
|
6250
|
+
for (const item of items) {
|
|
6251
|
+
const affected = await run.affected({
|
|
6252
|
+
model: target.name,
|
|
6253
|
+
method: "updateMany",
|
|
6254
|
+
args: { where: { AND: [item.where, fk] }, data: item.data }
|
|
6255
|
+
});
|
|
6256
|
+
if (affected === 0) {
|
|
6257
|
+
throw notFound({
|
|
6258
|
+
message: `nested "update" on ${link.relation.model}.${op.name}: no connected ${target.name} matched`,
|
|
6259
|
+
meta: { relation: op.name }
|
|
6127
6260
|
});
|
|
6128
|
-
if (affected === 0) {
|
|
6129
|
-
throw notFound({
|
|
6130
|
-
message: `nested "update" on ${link.relation.model}.${op.name}: no connected ${target.name} matched`,
|
|
6131
|
-
meta: { relation: op.name }
|
|
6132
|
-
});
|
|
6133
|
-
}
|
|
6134
6261
|
}
|
|
6135
6262
|
}
|
|
6136
6263
|
break;
|
|
@@ -6146,12 +6273,11 @@ async function applyChildUpdateOps(params) {
|
|
|
6146
6273
|
});
|
|
6147
6274
|
}
|
|
6148
6275
|
if (isM2M) {
|
|
6149
|
-
const members = await loadMemberKeys({ run, ctx, link, parentKey });
|
|
6150
6276
|
await run.affected({
|
|
6151
6277
|
model: target.name,
|
|
6152
6278
|
method: "updateMany",
|
|
6153
6279
|
args: {
|
|
6154
|
-
where: { AND: [item.where ?? {},
|
|
6280
|
+
where: { AND: [item.where ?? {}, await membershipFilter({ run, ctx, link, parentKey })] },
|
|
6155
6281
|
data
|
|
6156
6282
|
}
|
|
6157
6283
|
});
|
|
@@ -6169,35 +6295,17 @@ async function applyChildUpdateOps(params) {
|
|
|
6169
6295
|
const items = toItems({ op, verb, operand });
|
|
6170
6296
|
for (const item of items) {
|
|
6171
6297
|
if (isM2M) {
|
|
6172
|
-
const members = await loadMemberKeys({ run, ctx, link, parentKey });
|
|
6173
6298
|
const victims = await run.rows({
|
|
6174
6299
|
model: target.name,
|
|
6175
6300
|
method: "findMany",
|
|
6176
6301
|
args: {
|
|
6177
|
-
where: { AND: [item,
|
|
6302
|
+
where: { AND: [item, await membershipFilter({ run, ctx, link, parentKey })] },
|
|
6178
6303
|
select: childKeySelect({ link })
|
|
6179
6304
|
}
|
|
6180
6305
|
});
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
dialect: ctx.dialect,
|
|
6185
|
-
link,
|
|
6186
|
-
childKey: childTupleOf({ link, row: victim })[0]
|
|
6187
|
-
});
|
|
6188
|
-
}
|
|
6189
|
-
if (victims.length > 0) {
|
|
6190
|
-
await run.affected({
|
|
6191
|
-
model: target.name,
|
|
6192
|
-
method: "deleteMany",
|
|
6193
|
-
args: {
|
|
6194
|
-
where: childAssignment({
|
|
6195
|
-
link,
|
|
6196
|
-
values: [{ in: victims.map((victim) => childTupleOf({ link, row: victim })[0]) }]
|
|
6197
|
-
})
|
|
6198
|
-
}
|
|
6199
|
-
});
|
|
6200
|
-
}
|
|
6306
|
+
const childKeys = victims.map((victim) => childTupleOf({ link, row: victim })[0]);
|
|
6307
|
+
await deleteJoinRowsForChildren({ run, dialect: ctx.dialect, link, childKeys });
|
|
6308
|
+
await deleteChildrenByKey({ run, dialect: ctx.dialect, link, childKeys });
|
|
6201
6309
|
} else {
|
|
6202
6310
|
await run.affected({
|
|
6203
6311
|
model: target.name,
|
|
@@ -6251,12 +6359,12 @@ async function applyChildUpdateOps(params) {
|
|
|
6251
6359
|
});
|
|
6252
6360
|
if (found[0] !== undefined) {
|
|
6253
6361
|
if (isM2M) {
|
|
6254
|
-
await
|
|
6362
|
+
await assertM2MMembers({
|
|
6255
6363
|
run,
|
|
6256
6364
|
ctx,
|
|
6257
6365
|
link,
|
|
6258
6366
|
parentKey,
|
|
6259
|
-
|
|
6367
|
+
childKeys: [childTupleOf({ link, row: found[0] })[0]],
|
|
6260
6368
|
target: target.name,
|
|
6261
6369
|
verb: "upsert",
|
|
6262
6370
|
relation: op.name
|
|
@@ -6270,12 +6378,12 @@ async function applyChildUpdateOps(params) {
|
|
|
6270
6378
|
await run.single({ model: target.name, method: "update", args: { where: item.where, data: item.update } });
|
|
6271
6379
|
} else if (isM2M) {
|
|
6272
6380
|
const child = await runCreate({ ctx, run, model: target, data: item.create });
|
|
6273
|
-
await
|
|
6381
|
+
await insertJoinRows({
|
|
6274
6382
|
run,
|
|
6275
6383
|
dialect: ctx.dialect,
|
|
6276
6384
|
link,
|
|
6277
6385
|
parentKey,
|
|
6278
|
-
|
|
6386
|
+
childKeys: [childTupleOf({ link, row: child })[0]]
|
|
6279
6387
|
});
|
|
6280
6388
|
} else {
|
|
6281
6389
|
await runCreate({
|
|
@@ -6294,20 +6402,23 @@ async function applyChildUpdateOps(params) {
|
|
|
6294
6402
|
async function updateWithNested(params) {
|
|
6295
6403
|
const { ctx, model, where, data } = params;
|
|
6296
6404
|
const { scalars, ops } = partitionData({ meta: ctx.meta, model, data, phase: "update" });
|
|
6405
|
+
const ownerOps = ops.filter((op) => op.link.kind === "toOneOwner");
|
|
6406
|
+
const needsPreRead = ownerOps.length > 0 || Object.keys(scalars).length === 0;
|
|
6297
6407
|
return await ctx.transactional(async (run) => {
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6408
|
+
let pre;
|
|
6409
|
+
if (needsPreRead) {
|
|
6410
|
+
const preRows = await run.rows({ model: model.name, method: "findUnique", args: { where } });
|
|
6411
|
+
pre = preRows[0];
|
|
6412
|
+
if (pre === undefined) {
|
|
6413
|
+
throw notFound({
|
|
6414
|
+
message: `no ${model.name} record matched the where clause of update()`,
|
|
6415
|
+
meta: { model: model.name }
|
|
6416
|
+
});
|
|
6417
|
+
}
|
|
6305
6418
|
}
|
|
6306
6419
|
const fkSets = {};
|
|
6307
6420
|
const deferred = [];
|
|
6308
|
-
for (const op of
|
|
6309
|
-
if (op.link.kind !== "toOneOwner")
|
|
6310
|
-
continue;
|
|
6421
|
+
for (const op of ownerOps) {
|
|
6311
6422
|
await resolveOwnerUpdateVerb({ ctx, run, op, pre, fkSets, deferred });
|
|
6312
6423
|
}
|
|
6313
6424
|
assertWriteScope({ data, assignments: fkSets });
|
|
@@ -6674,22 +6785,35 @@ function parsePostgresPlan(params) {
|
|
|
6674
6785
|
};
|
|
6675
6786
|
}
|
|
6676
6787
|
var WRITE_LEADERS = ["INSERT", "UPDATE", "DELETE", "MERGE", "TRUNCATE", "CREATE", "DROP", "ALTER", "GRANT", "REVOKE", "COPY", "CALL", "DO"];
|
|
6788
|
+
var SIDE_EFFECT_CALLS = /\bpg_(?:try_)?advisory_(?:xact_)?lock(?:_shared)?\s*\(/i;
|
|
6677
6789
|
function refuse(params) {
|
|
6678
6790
|
return new VibeError13({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta ?? {} });
|
|
6679
6791
|
}
|
|
6680
|
-
function
|
|
6792
|
+
function executableReadRefusal(params) {
|
|
6681
6793
|
const normalized = normalizeSql({ text: params.text });
|
|
6682
6794
|
const body = normalized.replace(/;\s*$/, "");
|
|
6683
6795
|
if (body.includes(";")) {
|
|
6684
|
-
|
|
6796
|
+
return refuse({ message: "EXPLAIN ANALYZE refuses multi-statement text: it would execute every statement in it", meta: {} });
|
|
6685
6797
|
}
|
|
6686
6798
|
const leader = (body.match(/^\(*\s*([A-Za-z]+)/)?.[1] ?? "").toUpperCase();
|
|
6687
6799
|
if (WRITE_LEADERS.includes(leader)) {
|
|
6688
|
-
|
|
6800
|
+
return refuse({ message: `EXPLAIN ANALYZE refuses a ${leader} statement: the executing mode would perform the write`, meta: { statement: leader } });
|
|
6689
6801
|
}
|
|
6690
6802
|
if (leader !== "SELECT" && leader !== "WITH" && leader !== "TABLE" && leader !== "VALUES") {
|
|
6691
|
-
|
|
6803
|
+
return refuse({ message: `EXPLAIN ANALYZE only accepts read statements; this one starts with "${leader || "?"}"`, meta: { statement: leader } });
|
|
6804
|
+
}
|
|
6805
|
+
if (SIDE_EFFECT_CALLS.test(body)) {
|
|
6806
|
+
return refuse({ message: "EXPLAIN ANALYZE refuses a statement that takes an advisory lock: the executing mode would take the lock on the diagnostic session", meta: { statement: leader } });
|
|
6692
6807
|
}
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
function assertExecutableRead(params) {
|
|
6811
|
+
const refusal = executableReadRefusal(params);
|
|
6812
|
+
if (refusal !== undefined)
|
|
6813
|
+
throw refusal;
|
|
6814
|
+
}
|
|
6815
|
+
function isExecutableRead(params) {
|
|
6816
|
+
return executableReadRefusal(params) === undefined;
|
|
6693
6817
|
}
|
|
6694
6818
|
async function readPlanContext(params) {
|
|
6695
6819
|
const { executor, dialect, rlsContextKeys } = params;
|
|
@@ -6753,59 +6877,20 @@ async function explainSql(params) {
|
|
|
6753
6877
|
await run(session("sessionReadOnly"));
|
|
6754
6878
|
}
|
|
6755
6879
|
try {
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
query: "setContext",
|
|
6761
|
-
key: `${NATIVE_RLS.settingPrefix}${key}`,
|
|
6762
|
-
value: value === null || value === undefined ? null : String(value)
|
|
6763
|
-
});
|
|
6764
|
-
await run(statement);
|
|
6765
|
-
}
|
|
6766
|
-
const context = await readPlanContext({ executor, dialect, rlsContextKeys: contextKeys });
|
|
6767
|
-
if (!context.representative) {
|
|
6768
|
-
if (params.requireRepresentativeContext === true) {
|
|
6769
|
-
throw new VibeError13({
|
|
6770
|
-
code: "VIBE_RLS_CONTEXT",
|
|
6771
|
-
message: `refusing to report a plan taken under a non-representative identity: ${context.reason ?? "role bypasses row policies"}`,
|
|
6772
|
-
meta: { currentUser: context.currentUser, bypassRls: context.bypassRls, superuser: context.superuser }
|
|
6773
|
-
});
|
|
6774
|
-
}
|
|
6775
|
-
warnings.push(context.reason ?? "the plan was taken under a role that bypasses row policies");
|
|
6776
|
-
}
|
|
6777
|
-
const explain = renderPostgresExplain({
|
|
6778
|
-
dialect,
|
|
6779
|
-
query: { text: sql2.text, values: [...sql2.values], returning: "none" },
|
|
6780
|
-
options: mode === "analyze" ? { analyze: true, buffers: true } : {}
|
|
6781
|
-
});
|
|
6782
|
-
const rows = await run(explain);
|
|
6783
|
-
const payload = rows[0]?.["QUERY PLAN"] ?? rows[0]?.["query plan"] ?? rows[0]?.["queryPlan"];
|
|
6784
|
-
const parsed = parsePostgresPlan({
|
|
6785
|
-
payload,
|
|
6880
|
+
const report = await explainOnSession({
|
|
6881
|
+
executor,
|
|
6882
|
+
dialect: params.dialect,
|
|
6883
|
+
sql: sql2,
|
|
6786
6884
|
mode,
|
|
6885
|
+
warnings,
|
|
6886
|
+
...rlsContext === undefined ? {} : { rlsContext },
|
|
6887
|
+
...params.requireRepresentativeContext === undefined ? {} : { requireRepresentativeContext: params.requireRepresentativeContext },
|
|
6787
6888
|
...params.maxNodes === undefined ? {} : { maxNodes: params.maxNodes },
|
|
6788
6889
|
...params.includeLiterals === undefined ? {} : { includeLiterals: params.includeLiterals }
|
|
6789
6890
|
});
|
|
6790
|
-
if (parsed.truncated)
|
|
6791
|
-
warnings.push(`the plan was truncated at ${parsed.nodeCount} nodes; raise maxNodes to see the rest`);
|
|
6792
|
-
if (mode === "estimate") {
|
|
6793
|
-
warnings.push("estimated plan: costs are planner cost units (not milliseconds) and rows are estimates (not observations); EXPLAIN still parses, plans and takes planning locks");
|
|
6794
|
-
}
|
|
6795
6891
|
if (transactional)
|
|
6796
6892
|
await run(session("sessionCommit"));
|
|
6797
|
-
return
|
|
6798
|
-
mode,
|
|
6799
|
-
root: parsed.root,
|
|
6800
|
-
nodeCount: parsed.nodeCount,
|
|
6801
|
-
truncated: parsed.truncated,
|
|
6802
|
-
estimatedTotalCostUnits: parsed.root.estimatedTotalCost,
|
|
6803
|
-
...parsed.planningTimeMs === undefined ? {} : { planningTimeMs: parsed.planningTimeMs },
|
|
6804
|
-
...parsed.executionTimeMs === undefined ? {} : { executionTimeMs: parsed.executionTimeMs },
|
|
6805
|
-
literalsRedacted: params.includeLiterals !== true,
|
|
6806
|
-
context,
|
|
6807
|
-
warnings
|
|
6808
|
-
};
|
|
6893
|
+
return report;
|
|
6809
6894
|
} catch (error) {
|
|
6810
6895
|
if (transactional) {
|
|
6811
6896
|
await run(session("sessionRollback")).catch(() => []);
|
|
@@ -6813,6 +6898,64 @@ async function explainSql(params) {
|
|
|
6813
6898
|
throw error;
|
|
6814
6899
|
}
|
|
6815
6900
|
}
|
|
6901
|
+
async function explainOnSession(params) {
|
|
6902
|
+
const { executor, sql: sql2, mode, rlsContext } = params;
|
|
6903
|
+
const dialect = requirePostgres({ dialect: params.dialect });
|
|
6904
|
+
const warnings = params.warnings ?? [];
|
|
6905
|
+
const contextKeys = rlsContext === undefined ? [] : Object.keys(rlsContext);
|
|
6906
|
+
const run = async (query) => executor({ text: query.text, values: query.values });
|
|
6907
|
+
for (const key of contextKeys) {
|
|
6908
|
+
const value = rlsContext?.[key];
|
|
6909
|
+
const statement = renderPostgresWorkload({
|
|
6910
|
+
dialect,
|
|
6911
|
+
query: "setContext",
|
|
6912
|
+
key: `${NATIVE_RLS.settingPrefix}${key}`,
|
|
6913
|
+
value: value === null || value === undefined ? null : String(value)
|
|
6914
|
+
});
|
|
6915
|
+
await run(statement);
|
|
6916
|
+
}
|
|
6917
|
+
const context = await readPlanContext({ executor, dialect, rlsContextKeys: contextKeys });
|
|
6918
|
+
if (!context.representative) {
|
|
6919
|
+
if (params.requireRepresentativeContext === true) {
|
|
6920
|
+
throw new VibeError13({
|
|
6921
|
+
code: "VIBE_RLS_CONTEXT",
|
|
6922
|
+
message: `refusing to report a plan taken under a non-representative identity: ${context.reason ?? "role bypasses row policies"}`,
|
|
6923
|
+
meta: { currentUser: context.currentUser, bypassRls: context.bypassRls, superuser: context.superuser }
|
|
6924
|
+
});
|
|
6925
|
+
}
|
|
6926
|
+
warnings.push(context.reason ?? "the plan was taken under a role that bypasses row policies");
|
|
6927
|
+
}
|
|
6928
|
+
const explain = renderPostgresExplain({
|
|
6929
|
+
dialect,
|
|
6930
|
+
query: { text: sql2.text, values: [...sql2.values], returning: "none" },
|
|
6931
|
+
options: mode === "analyze" ? { analyze: true, buffers: true } : {}
|
|
6932
|
+
});
|
|
6933
|
+
const rows = await run(explain);
|
|
6934
|
+
const payload = rows[0]?.["QUERY PLAN"] ?? rows[0]?.["query plan"] ?? rows[0]?.["queryPlan"];
|
|
6935
|
+
const parsed = parsePostgresPlan({
|
|
6936
|
+
payload,
|
|
6937
|
+
mode,
|
|
6938
|
+
...params.maxNodes === undefined ? {} : { maxNodes: params.maxNodes },
|
|
6939
|
+
...params.includeLiterals === undefined ? {} : { includeLiterals: params.includeLiterals }
|
|
6940
|
+
});
|
|
6941
|
+
if (parsed.truncated)
|
|
6942
|
+
warnings.push(`the plan was truncated at ${parsed.nodeCount} nodes; raise maxNodes to see the rest`);
|
|
6943
|
+
if (mode === "estimate") {
|
|
6944
|
+
warnings.push("estimated plan: costs are planner cost units (not milliseconds) and rows are estimates (not observations); EXPLAIN still parses, plans and takes planning locks");
|
|
6945
|
+
}
|
|
6946
|
+
return {
|
|
6947
|
+
mode,
|
|
6948
|
+
root: parsed.root,
|
|
6949
|
+
nodeCount: parsed.nodeCount,
|
|
6950
|
+
truncated: parsed.truncated,
|
|
6951
|
+
estimatedTotalCostUnits: parsed.root.estimatedTotalCost,
|
|
6952
|
+
...parsed.planningTimeMs === undefined ? {} : { planningTimeMs: parsed.planningTimeMs },
|
|
6953
|
+
...parsed.executionTimeMs === undefined ? {} : { executionTimeMs: parsed.executionTimeMs },
|
|
6954
|
+
literalsRedacted: params.includeLiterals !== true,
|
|
6955
|
+
context,
|
|
6956
|
+
warnings
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6816
6959
|
async function explainOperation(params) {
|
|
6817
6960
|
const { schema, model, method, args } = params;
|
|
6818
6961
|
const dialect = requirePostgres({ dialect: params.dialect });
|
|
@@ -6847,9 +6990,253 @@ async function explainOperation(params) {
|
|
|
6847
6990
|
...params.includeLiterals === undefined ? {} : { includeLiterals: params.includeLiterals }
|
|
6848
6991
|
});
|
|
6849
6992
|
}
|
|
6993
|
+
// packages/runtime/src/diagnostics/statement-diagnostics.ts
|
|
6994
|
+
import { VibeError as VibeError14 } from "@vibeorm/schema";
|
|
6995
|
+
import { renderPostgresWorkload as renderPostgresWorkload2 } from "@vibeorm/sql";
|
|
6996
|
+
var STATEMENT_DIAGNOSTICS_DEFAULTS = Object.freeze({
|
|
6997
|
+
slowerThanMs: 0,
|
|
6998
|
+
explain: "off",
|
|
6999
|
+
explainTimeoutMs: 5000,
|
|
7000
|
+
analyzeWarnBelowMs: 1,
|
|
7001
|
+
envVariable: "VIBEORM_DIAGNOSTICS"
|
|
7002
|
+
});
|
|
7003
|
+
var EXPLAIN_MODES = ["off", "estimate", "analyze"];
|
|
7004
|
+
function configError(params) {
|
|
7005
|
+
return new VibeError14({ code: "VIBE_CONFIG", message: params.message, meta: params.meta ?? {} });
|
|
7006
|
+
}
|
|
7007
|
+
function isExplainMode(value) {
|
|
7008
|
+
return typeof value === "string" && EXPLAIN_MODES.includes(value);
|
|
7009
|
+
}
|
|
7010
|
+
function assertThreshold(params) {
|
|
7011
|
+
const { value, source } = params;
|
|
7012
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
7013
|
+
throw configError({
|
|
7014
|
+
message: `${source}: slowerThanMs must be a non-negative number of milliseconds, got ${String(value)}`,
|
|
7015
|
+
meta: { source, value }
|
|
7016
|
+
});
|
|
7017
|
+
}
|
|
7018
|
+
return value;
|
|
7019
|
+
}
|
|
7020
|
+
function parseDiagnosticsEnv(params) {
|
|
7021
|
+
const raw = params.value?.trim();
|
|
7022
|
+
if (raw === undefined || raw === "")
|
|
7023
|
+
return;
|
|
7024
|
+
const variable = STATEMENT_DIAGNOSTICS_DEFAULTS.envVariable;
|
|
7025
|
+
const [modePart, thresholdPart, ...rest] = raw.split(":");
|
|
7026
|
+
const mode = modePart?.trim().toLowerCase();
|
|
7027
|
+
if (rest.length > 0 || !isExplainMode(mode)) {
|
|
7028
|
+
throw configError({
|
|
7029
|
+
message: `${variable}="${raw}" is not <explain>[:<slowerThanMs>] with explain one of ${EXPLAIN_MODES.join(" | ")} \u2014 e.g. ${variable}=analyze:5`,
|
|
7030
|
+
meta: { variable, value: raw }
|
|
7031
|
+
});
|
|
7032
|
+
}
|
|
7033
|
+
if (thresholdPart === undefined)
|
|
7034
|
+
return { explain: mode, slowerThanMs: STATEMENT_DIAGNOSTICS_DEFAULTS.slowerThanMs };
|
|
7035
|
+
const threshold = thresholdPart.trim();
|
|
7036
|
+
const parsed = /^\d+(?:\.\d+)?$/.test(threshold) ? Number(threshold) : Number.NaN;
|
|
7037
|
+
if (!Number.isFinite(parsed)) {
|
|
7038
|
+
throw configError({
|
|
7039
|
+
message: `${variable}="${raw}": the threshold after ":" must be a non-negative number of milliseconds`,
|
|
7040
|
+
meta: { variable, value: raw }
|
|
7041
|
+
});
|
|
7042
|
+
}
|
|
7043
|
+
return { explain: mode, slowerThanMs: parsed };
|
|
7044
|
+
}
|
|
7045
|
+
function formatStatementDiagnostic(params) {
|
|
7046
|
+
const { report } = params;
|
|
7047
|
+
const head = `[vibeorm] ${report.model ?? "raw"}.${report.method} ${report.durationMs.toFixed(1)} ms \xB7 ${report.parameterCount} param${report.parameterCount === 1 ? "" : "s"}${report.inTransaction ? " \xB7 in transaction" : ""}`;
|
|
7048
|
+
if (report.plan !== undefined)
|
|
7049
|
+
return `${head} \xB7 ${report.plan.summary}`;
|
|
7050
|
+
if (report.explainSkipped !== undefined) {
|
|
7051
|
+
const detail = report.explainError === undefined ? "" : ` (${report.explainError})`;
|
|
7052
|
+
return `${head} \xB7 explain skipped: ${report.explainSkipped}${detail}`;
|
|
7053
|
+
}
|
|
7054
|
+
return head;
|
|
7055
|
+
}
|
|
7056
|
+
var defaultSink = (report) => {
|
|
7057
|
+
console.warn(formatStatementDiagnostic({ report }));
|
|
7058
|
+
};
|
|
7059
|
+
function resolveStatementDiagnostics(params) {
|
|
7060
|
+
const { options } = params;
|
|
7061
|
+
const fromEnv = parseDiagnosticsEnv({ value: params.env });
|
|
7062
|
+
if (options === undefined && fromEnv === undefined)
|
|
7063
|
+
return;
|
|
7064
|
+
const explain = options?.explain ?? fromEnv?.explain ?? STATEMENT_DIAGNOSTICS_DEFAULTS.explain;
|
|
7065
|
+
if (!isExplainMode(explain)) {
|
|
7066
|
+
throw configError({
|
|
7067
|
+
message: `diagnostics.explain must be one of ${EXPLAIN_MODES.join(" | ")}, got ${String(explain)}`,
|
|
7068
|
+
meta: { explain }
|
|
7069
|
+
});
|
|
7070
|
+
}
|
|
7071
|
+
const slowerThanMs = options?.slowerThanMs === undefined ? fromEnv?.slowerThanMs ?? STATEMENT_DIAGNOSTICS_DEFAULTS.slowerThanMs : assertThreshold({ value: options.slowerThanMs, source: "diagnostics" });
|
|
7072
|
+
if (options?.sink !== undefined && typeof options.sink !== "function") {
|
|
7073
|
+
throw configError({ message: "diagnostics.sink must be a function", meta: {} });
|
|
7074
|
+
}
|
|
7075
|
+
return {
|
|
7076
|
+
explain,
|
|
7077
|
+
slowerThanMs,
|
|
7078
|
+
sink: options?.sink ?? defaultSink,
|
|
7079
|
+
showLiterals: options?.showLiterals === true,
|
|
7080
|
+
fromEnvironment: fromEnv !== undefined && (options?.explain === undefined || options.slowerThanMs === undefined)
|
|
7081
|
+
};
|
|
7082
|
+
}
|
|
7083
|
+
function nodeLabel(node) {
|
|
7084
|
+
const target = node.relation ?? node.indexName;
|
|
7085
|
+
return target === undefined ? node.nodeType : `${node.nodeType} on ${target}`;
|
|
7086
|
+
}
|
|
7087
|
+
function collectLoops(params) {
|
|
7088
|
+
const out = [];
|
|
7089
|
+
const walk = (node) => {
|
|
7090
|
+
if (node.actualLoops !== undefined && node.actualLoops > 1) {
|
|
7091
|
+
out.push({ node: nodeLabel(node), loops: node.actualLoops, rows: node.actualRows ?? 0 });
|
|
7092
|
+
}
|
|
7093
|
+
for (const child of node.children)
|
|
7094
|
+
walk(child);
|
|
7095
|
+
};
|
|
7096
|
+
walk(params.root);
|
|
7097
|
+
out.sort((a, b) => b.loops - a.loops);
|
|
7098
|
+
return out;
|
|
7099
|
+
}
|
|
7100
|
+
var formatCount = (value) => value.toLocaleString("en-US");
|
|
7101
|
+
function summarizePlan(params) {
|
|
7102
|
+
const { plan, mode } = params;
|
|
7103
|
+
const root = plan.root;
|
|
7104
|
+
const loops = mode === "analyze" ? collectLoops({ root }) : [];
|
|
7105
|
+
const parts = [];
|
|
7106
|
+
if (mode === "analyze") {
|
|
7107
|
+
const executionMs = plan.executionTimeMs ?? root.actualTotalTimeMs;
|
|
7108
|
+
parts.push(executionMs === undefined ? nodeLabel(root) : `${nodeLabel(root)} ${executionMs.toFixed(1)} ms`);
|
|
7109
|
+
const hottest = loops[0];
|
|
7110
|
+
if (hottest !== undefined)
|
|
7111
|
+
parts.push(`${hottest.node} loops=${formatCount(hottest.loops)} rows=${formatCount(hottest.rows)}`);
|
|
7112
|
+
if (root.sharedBlocksHit !== undefined)
|
|
7113
|
+
parts.push(`shared hit ${formatCount(root.sharedBlocksHit)}`);
|
|
7114
|
+
if (root.sharedBlocksRead !== undefined && root.sharedBlocksRead > 0)
|
|
7115
|
+
parts.push(`shared read ${formatCount(root.sharedBlocksRead)}`);
|
|
7116
|
+
} else {
|
|
7117
|
+
parts.push(`${nodeLabel(root)} cost=${root.estimatedTotalCost} rows\u2248${formatCount(root.estimatedRows)} (estimated)`);
|
|
7118
|
+
}
|
|
7119
|
+
if (plan.truncated)
|
|
7120
|
+
parts.push(`plan truncated at ${plan.nodeCount} nodes`);
|
|
7121
|
+
return {
|
|
7122
|
+
summary: parts.join(" \xB7 "),
|
|
7123
|
+
nodes: plan,
|
|
7124
|
+
...plan.executionTimeMs === undefined ? {} : { executionMs: plan.executionTimeMs },
|
|
7125
|
+
...plan.planningTimeMs === undefined ? {} : { planningMs: plan.planningTimeMs },
|
|
7126
|
+
...root.sharedBlocksHit === undefined ? {} : { buffersShared: root.sharedBlocksHit },
|
|
7127
|
+
...loops.length === 0 ? {} : { loops }
|
|
7128
|
+
};
|
|
7129
|
+
}
|
|
7130
|
+
function errorCode(error) {
|
|
7131
|
+
return error instanceof VibeError14 ? error.code : "VIBE_INTERNAL";
|
|
7132
|
+
}
|
|
7133
|
+
function createStatementDiagnostics(params) {
|
|
7134
|
+
const { config, adapter, dialect } = params;
|
|
7135
|
+
const outstanding = new Set;
|
|
7136
|
+
const explainable = dialect.name === "postgres";
|
|
7137
|
+
const timeoutMs = STATEMENT_DIAGNOSTICS_DEFAULTS.explainTimeoutMs;
|
|
7138
|
+
const transactionOptions = adapter.budgets?.statementTimeout === "all-statements" ? { timeout: timeoutMs } : undefined;
|
|
7139
|
+
const report = (diagnostic) => {
|
|
7140
|
+
try {
|
|
7141
|
+
config.sink(diagnostic);
|
|
7142
|
+
} catch {}
|
|
7143
|
+
};
|
|
7144
|
+
const explain = async (statement, mode) => {
|
|
7145
|
+
const run = async (tx) => {
|
|
7146
|
+
if (mode === "analyze") {
|
|
7147
|
+
const readOnly = renderPostgresWorkload2({ dialect, query: "sessionReadOnly" });
|
|
7148
|
+
await tx.execute({ text: readOnly.text, values: readOnly.values });
|
|
7149
|
+
}
|
|
7150
|
+
const executor = statement.raw ? async (query) => (await tx.executeUnsafe({ text: query.text, values: query.values ?? [] })).rows : async (query) => tx.execute({
|
|
7151
|
+
text: query.text,
|
|
7152
|
+
values: (query.values ?? []).map((value) => Array.isArray(value) ? tx.formatArrayParam(value) : value)
|
|
7153
|
+
});
|
|
7154
|
+
const report2 = await explainOnSession({
|
|
7155
|
+
executor,
|
|
7156
|
+
dialect: dialect.name,
|
|
7157
|
+
sql: { text: statement.text, values: statement.values },
|
|
7158
|
+
mode,
|
|
7159
|
+
includeLiterals: config.showLiterals
|
|
7160
|
+
});
|
|
7161
|
+
return summarizePlan({
|
|
7162
|
+
plan: {
|
|
7163
|
+
root: report2.root,
|
|
7164
|
+
nodeCount: report2.nodeCount,
|
|
7165
|
+
truncated: report2.truncated,
|
|
7166
|
+
...report2.planningTimeMs === undefined ? {} : { planningTimeMs: report2.planningTimeMs },
|
|
7167
|
+
...report2.executionTimeMs === undefined ? {} : { executionTimeMs: report2.executionTimeMs }
|
|
7168
|
+
},
|
|
7169
|
+
mode
|
|
7170
|
+
});
|
|
7171
|
+
};
|
|
7172
|
+
return transactionOptions === undefined ? adapter.transaction(run) : adapter.transaction(run, transactionOptions);
|
|
7173
|
+
};
|
|
7174
|
+
const schedule = (statement, mode, base) => {
|
|
7175
|
+
const task = new Promise((resolve) => {
|
|
7176
|
+
setTimeout(() => {
|
|
7177
|
+
explain(statement, mode).then((plan) => {
|
|
7178
|
+
report({ ...base, plan });
|
|
7179
|
+
resolve();
|
|
7180
|
+
}, (error) => {
|
|
7181
|
+
report({ ...base, explainSkipped: "error", explainError: errorCode(error) });
|
|
7182
|
+
resolve();
|
|
7183
|
+
});
|
|
7184
|
+
}, 0);
|
|
7185
|
+
});
|
|
7186
|
+
outstanding.add(task);
|
|
7187
|
+
task.finally(() => outstanding.delete(task));
|
|
7188
|
+
};
|
|
7189
|
+
return {
|
|
7190
|
+
config,
|
|
7191
|
+
observe: (statement) => {
|
|
7192
|
+
if (statement.durationMs < config.slowerThanMs)
|
|
7193
|
+
return;
|
|
7194
|
+
const base = {
|
|
7195
|
+
model: statement.model,
|
|
7196
|
+
method: statement.method,
|
|
7197
|
+
text: statement.text,
|
|
7198
|
+
durationMs: statement.durationMs,
|
|
7199
|
+
parameterCount: statement.values.length,
|
|
7200
|
+
inTransaction: statement.inTransaction
|
|
7201
|
+
};
|
|
7202
|
+
if (config.explain === "off") {
|
|
7203
|
+
report(base);
|
|
7204
|
+
return;
|
|
7205
|
+
}
|
|
7206
|
+
if (!explainable) {
|
|
7207
|
+
report({ ...base, explainSkipped: "dialect" });
|
|
7208
|
+
return;
|
|
7209
|
+
}
|
|
7210
|
+
if (statement.inTransaction) {
|
|
7211
|
+
report({ ...base, explainSkipped: "in-transaction" });
|
|
7212
|
+
return;
|
|
7213
|
+
}
|
|
7214
|
+
if (!isExecutableRead({ text: statement.text })) {
|
|
7215
|
+
report({ ...base, explainSkipped: "not-a-read" });
|
|
7216
|
+
return;
|
|
7217
|
+
}
|
|
7218
|
+
schedule(statement, config.explain, base);
|
|
7219
|
+
},
|
|
7220
|
+
flush: async () => {
|
|
7221
|
+
if (outstanding.size === 0)
|
|
7222
|
+
return;
|
|
7223
|
+
let timer;
|
|
7224
|
+
const bound = new Promise((resolve) => {
|
|
7225
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
7226
|
+
});
|
|
7227
|
+
try {
|
|
7228
|
+
await Promise.race([Promise.allSettled([...outstanding]), bound]);
|
|
7229
|
+
} finally {
|
|
7230
|
+
if (timer !== undefined)
|
|
7231
|
+
clearTimeout(timer);
|
|
7232
|
+
}
|
|
7233
|
+
},
|
|
7234
|
+
outstanding: () => outstanding.size
|
|
7235
|
+
};
|
|
7236
|
+
}
|
|
6850
7237
|
// packages/runtime/src/diagnostics/workload.ts
|
|
6851
7238
|
import { unsupportedCapability as unsupportedCapability6 } from "@vibeorm/schema";
|
|
6852
|
-
import { getSqlDialect as getSqlDialect3, renderPostgresWorkload as
|
|
7239
|
+
import { getSqlDialect as getSqlDialect3, renderPostgresWorkload as renderPostgresWorkload3 } from "@vibeorm/sql";
|
|
6853
7240
|
var STATEMENTS_INFO_MIN_VERSION = 140000;
|
|
6854
7241
|
var STATEMENTS_MIN_VERSION = 130000;
|
|
6855
7242
|
var NOTES = Object.freeze({
|
|
@@ -6864,7 +7251,7 @@ function num2(value) {
|
|
|
6864
7251
|
return typeof value === "number" ? value : Number(value ?? 0);
|
|
6865
7252
|
}
|
|
6866
7253
|
async function readAvailability(params) {
|
|
6867
|
-
const query =
|
|
7254
|
+
const query = renderPostgresWorkload3({ dialect: params.dialect, query: "statementExtension" });
|
|
6868
7255
|
const rows = await params.executor({ text: query.text, values: query.values });
|
|
6869
7256
|
const row = rows[0] ?? {};
|
|
6870
7257
|
const installedVersion = row["installedVersion"] === null || row["installedVersion"] === undefined ? null : String(row["installedVersion"]);
|
|
@@ -6916,14 +7303,14 @@ async function readWorkloadStatistics(params) {
|
|
|
6916
7303
|
notes.push(NOTES.restrictedText);
|
|
6917
7304
|
let statsReset = null;
|
|
6918
7305
|
if (availability.serverVersionNum >= STATEMENTS_INFO_MIN_VERSION) {
|
|
6919
|
-
const info =
|
|
7306
|
+
const info = renderPostgresWorkload3({ dialect, query: "statementInfo", schema: availability.schema });
|
|
6920
7307
|
const infoRows = await executor({ text: info.text, values: info.values });
|
|
6921
7308
|
const value = infoRows[0]?.["statsReset"];
|
|
6922
7309
|
statsReset = value === null || value === undefined ? null : String(value);
|
|
6923
7310
|
} else {
|
|
6924
7311
|
notes.push("this server predates pg_stat_statements_info, so the collection window's reset time is unknown");
|
|
6925
7312
|
}
|
|
6926
|
-
const snapshot =
|
|
7313
|
+
const snapshot = renderPostgresWorkload3({
|
|
6927
7314
|
dialect,
|
|
6928
7315
|
query: "statementSnapshot",
|
|
6929
7316
|
schema: availability.schema,
|
|
@@ -6959,7 +7346,7 @@ async function readWorkloadStatistics(params) {
|
|
|
6959
7346
|
};
|
|
6960
7347
|
}
|
|
6961
7348
|
// packages/runtime/src/diagnostics/insight.ts
|
|
6962
|
-
import { VibeError as
|
|
7349
|
+
import { VibeError as VibeError15 } from "@vibeorm/schema";
|
|
6963
7350
|
var DIAGNOSTIC_THRESHOLDS = Object.freeze({
|
|
6964
7351
|
repeatThreshold: 5,
|
|
6965
7352
|
minRepeatThreshold: 2,
|
|
@@ -6977,7 +7364,7 @@ function bounded(params) {
|
|
|
6977
7364
|
if (value === undefined)
|
|
6978
7365
|
return fallback;
|
|
6979
7366
|
if (!Number.isInteger(value) || value < min || value > max) {
|
|
6980
|
-
throw new
|
|
7367
|
+
throw new VibeError15({
|
|
6981
7368
|
code: "VIBE_CONFIG",
|
|
6982
7369
|
message: `${name} must be an integer between ${min} and ${max}, got ${String(value)}`,
|
|
6983
7370
|
meta: { [name]: value }
|
|
@@ -7200,11 +7587,11 @@ function recommendFromPlan(params) {
|
|
|
7200
7587
|
return findings.slice(0, maxFindings);
|
|
7201
7588
|
}
|
|
7202
7589
|
// packages/runtime/src/relation-loader.ts
|
|
7203
|
-
import { VibeError as
|
|
7590
|
+
import { VibeError as VibeError17 } from "@vibeorm/schema";
|
|
7204
7591
|
import { renderStatement as renderStatement3 } from "@vibeorm/sql";
|
|
7205
7592
|
|
|
7206
7593
|
// packages/runtime/src/computed.ts
|
|
7207
|
-
import { VibeError as
|
|
7594
|
+
import { VibeError as VibeError16 } from "@vibeorm/schema";
|
|
7208
7595
|
function sameNeeds(a, b) {
|
|
7209
7596
|
if (a.length !== b.length)
|
|
7210
7597
|
return false;
|
|
@@ -7240,7 +7627,7 @@ function assertComputedManifest(params) {
|
|
|
7240
7627
|
problems.push(`"${key}" is configured but missing from the generated client`);
|
|
7241
7628
|
}
|
|
7242
7629
|
if (problems.length > 0) {
|
|
7243
|
-
throw new
|
|
7630
|
+
throw new VibeError16({
|
|
7244
7631
|
code: "VIBE_STALE_CLIENT",
|
|
7245
7632
|
message: `computed-field config changed since generate; run vibeorm generate \u2014 ${problems.join("; ")}`,
|
|
7246
7633
|
meta: { problems }
|
|
@@ -7263,6 +7650,87 @@ function applyComputedFields(params) {
|
|
|
7263
7650
|
}
|
|
7264
7651
|
|
|
7265
7652
|
// packages/runtime/src/relation-loader.ts
|
|
7653
|
+
function resolveRelationConcurrency(params) {
|
|
7654
|
+
const { concurrency } = params;
|
|
7655
|
+
if (concurrency === undefined)
|
|
7656
|
+
return 1;
|
|
7657
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
7658
|
+
throw new VibeError17({
|
|
7659
|
+
code: "VIBE_CONFIG",
|
|
7660
|
+
message: `relationLoading.concurrency must be a positive integer \u2014 got ${String(concurrency)}`,
|
|
7661
|
+
meta: { concurrency }
|
|
7662
|
+
});
|
|
7663
|
+
}
|
|
7664
|
+
return concurrency;
|
|
7665
|
+
}
|
|
7666
|
+
function createStatementLimiter(params) {
|
|
7667
|
+
const { concurrency } = params;
|
|
7668
|
+
const waiting = [];
|
|
7669
|
+
let active = 0;
|
|
7670
|
+
const release = () => {
|
|
7671
|
+
const next = waiting.shift();
|
|
7672
|
+
if (next === undefined) {
|
|
7673
|
+
active -= 1;
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
next();
|
|
7677
|
+
};
|
|
7678
|
+
return {
|
|
7679
|
+
run: async (task) => {
|
|
7680
|
+
if (active >= concurrency) {
|
|
7681
|
+
await new Promise((resolve) => {
|
|
7682
|
+
waiting.push(resolve);
|
|
7683
|
+
});
|
|
7684
|
+
} else {
|
|
7685
|
+
active += 1;
|
|
7686
|
+
}
|
|
7687
|
+
try {
|
|
7688
|
+
return await task();
|
|
7689
|
+
} finally {
|
|
7690
|
+
release();
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
};
|
|
7694
|
+
}
|
|
7695
|
+
async function runAllSettled(params) {
|
|
7696
|
+
const settled = await Promise.allSettled(params.tasks.map((task) => task()));
|
|
7697
|
+
for (const result of settled) {
|
|
7698
|
+
if (result.status === "rejected")
|
|
7699
|
+
throw result.reason;
|
|
7700
|
+
}
|
|
7701
|
+
}
|
|
7702
|
+
function createLoaderContext(params) {
|
|
7703
|
+
const { dialect, meta, execute, wire } = params;
|
|
7704
|
+
const concurrency = resolveRelationConcurrency({ concurrency: params.concurrency });
|
|
7705
|
+
if (concurrency === 1)
|
|
7706
|
+
return { dialect, meta, execute, wire, limiter: null };
|
|
7707
|
+
const limiter = createStatementLimiter({ concurrency });
|
|
7708
|
+
return {
|
|
7709
|
+
dialect,
|
|
7710
|
+
meta,
|
|
7711
|
+
execute: (statement) => limiter.run(() => execute(statement)),
|
|
7712
|
+
wire,
|
|
7713
|
+
limiter
|
|
7714
|
+
};
|
|
7715
|
+
}
|
|
7716
|
+
async function executeChunks(params) {
|
|
7717
|
+
const { ctx, chunks, run } = params;
|
|
7718
|
+
const raw = [];
|
|
7719
|
+
if (ctx.limiter === null || chunks.length < 2) {
|
|
7720
|
+
for (const chunk of chunks)
|
|
7721
|
+
pushAll(raw, await run(chunk));
|
|
7722
|
+
return raw;
|
|
7723
|
+
}
|
|
7724
|
+
const perChunk = chunks.map(() => []);
|
|
7725
|
+
await runAllSettled({
|
|
7726
|
+
tasks: chunks.map((chunk, index) => async () => {
|
|
7727
|
+
perChunk[index] = await run(chunk);
|
|
7728
|
+
})
|
|
7729
|
+
});
|
|
7730
|
+
for (const rows of perChunk)
|
|
7731
|
+
pushAll(raw, rows);
|
|
7732
|
+
return raw;
|
|
7733
|
+
}
|
|
7266
7734
|
var COUNT_VALUE_ALIAS = "__vibe_count";
|
|
7267
7735
|
var WINDOW_ROW_ALIAS = "__vibe_rn";
|
|
7268
7736
|
var WINDOW_TABLE_ALIAS = "__vibe_win";
|
|
@@ -7372,7 +7840,7 @@ function chunkKeys(params) {
|
|
|
7372
7840
|
const rendered = renderStatement3({ dialect: ctx.dialect, statement: build(keys.slice(0, 1)) });
|
|
7373
7841
|
fixed = rendered.values.length - (expands ? arity : 1);
|
|
7374
7842
|
if (fixed + arity > budget) {
|
|
7375
|
-
throw new
|
|
7843
|
+
throw new VibeError17({
|
|
7376
7844
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
7377
7845
|
message: `relation "${relation}" needs ${fixed + arity} bound parameters for its own predicate, more than ${ctx.dialect.name}'s ${budget}-parameter statement budget \u2014 narrow the nested filter`,
|
|
7378
7846
|
meta: { relation, dialect: ctx.dialect.name, required: fixed + arity, budget }
|
|
@@ -7411,7 +7879,6 @@ async function loadToManyFk(params) {
|
|
|
7411
7879
|
}
|
|
7412
7880
|
}
|
|
7413
7881
|
});
|
|
7414
|
-
const raw = [];
|
|
7415
7882
|
const chunks = chunkKeys({
|
|
7416
7883
|
ctx,
|
|
7417
7884
|
keys,
|
|
@@ -7420,10 +7887,11 @@ async function loadToManyFk(params) {
|
|
|
7420
7887
|
measure: node.args.where !== undefined || bounds !== null,
|
|
7421
7888
|
arity: link.childKeys.length
|
|
7422
7889
|
});
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7890
|
+
const raw = await executeChunks({
|
|
7891
|
+
ctx,
|
|
7892
|
+
chunks,
|
|
7893
|
+
run: (chunk) => ctx.execute({ statement: build(chunk), model: model.name, method: `include.${node.name}` })
|
|
7894
|
+
});
|
|
7427
7895
|
const children = materializeRows({ dialect: ctx.dialect.name, model: link.target, rows: raw, wire: ctx.wire });
|
|
7428
7896
|
const grouped = new Map;
|
|
7429
7897
|
for (const child of children) {
|
|
@@ -7459,7 +7927,6 @@ async function loadToOne(params) {
|
|
|
7459
7927
|
node,
|
|
7460
7928
|
membership: membershipExpr({ ctx, fields: link.childKeys, encodeAs: link.childKeys, tuples: chunk })
|
|
7461
7929
|
});
|
|
7462
|
-
const raw = [];
|
|
7463
7930
|
const chunks = chunkKeys({
|
|
7464
7931
|
ctx,
|
|
7465
7932
|
keys,
|
|
@@ -7468,10 +7935,11 @@ async function loadToOne(params) {
|
|
|
7468
7935
|
measure: node.args.where !== undefined,
|
|
7469
7936
|
arity: link.childKeys.length
|
|
7470
7937
|
});
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7938
|
+
const raw = await executeChunks({
|
|
7939
|
+
ctx,
|
|
7940
|
+
chunks,
|
|
7941
|
+
run: (chunk) => ctx.execute({ statement: build(chunk), model: model.name, method: `include.${node.name}` })
|
|
7942
|
+
});
|
|
7475
7943
|
const children = materializeRows({ dialect: ctx.dialect.name, model: link.target, rows: raw, wire: ctx.wire });
|
|
7476
7944
|
const byKey = new Map;
|
|
7477
7945
|
for (const child of children) {
|
|
@@ -7502,7 +7970,7 @@ async function loadManyToMany(params) {
|
|
|
7502
7970
|
const link = node.link;
|
|
7503
7971
|
const join = link.join;
|
|
7504
7972
|
if (join === null) {
|
|
7505
|
-
throw new
|
|
7973
|
+
throw new VibeError17({
|
|
7506
7974
|
code: "VIBE_INTERNAL",
|
|
7507
7975
|
message: `manyToMany relation "${node.name}" resolved without join-table wiring`
|
|
7508
7976
|
});
|
|
@@ -7575,7 +8043,6 @@ async function loadManyToMany(params) {
|
|
|
7575
8043
|
};
|
|
7576
8044
|
return windowedStatement({ inner, columns: outerColumns, bounds });
|
|
7577
8045
|
};
|
|
7578
|
-
const raw = [];
|
|
7579
8046
|
const chunks = chunkKeys({
|
|
7580
8047
|
ctx,
|
|
7581
8048
|
keys,
|
|
@@ -7584,10 +8051,11 @@ async function loadManyToMany(params) {
|
|
|
7584
8051
|
measure: node.args.where !== undefined || bounds !== null,
|
|
7585
8052
|
arity: 1
|
|
7586
8053
|
});
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
8054
|
+
const raw = await executeChunks({
|
|
8055
|
+
ctx,
|
|
8056
|
+
chunks,
|
|
8057
|
+
run: (chunk) => ctx.execute({ statement: build(chunk), model: model.name, method: `include.${node.name}` })
|
|
8058
|
+
});
|
|
7591
8059
|
const pairs = materializeRows({ dialect: ctx.dialect.name, model: target, rows: raw, wire: ctx.wire });
|
|
7592
8060
|
const decodeParent = fieldDecoder({
|
|
7593
8061
|
dialect: ctx.dialect.name,
|
|
@@ -7614,16 +8082,19 @@ async function loadManyToMany(params) {
|
|
|
7614
8082
|
}
|
|
7615
8083
|
return kept;
|
|
7616
8084
|
}
|
|
7617
|
-
|
|
7618
|
-
const {
|
|
7619
|
-
if (counts.length === 0 || rows.length === 0)
|
|
7620
|
-
return;
|
|
8085
|
+
function seedCountBuckets(params) {
|
|
8086
|
+
const { rows, counts } = params;
|
|
7621
8087
|
for (const row of rows) {
|
|
7622
8088
|
const bucket = {};
|
|
7623
8089
|
for (const node of counts)
|
|
7624
8090
|
bucket[node.name] = 0;
|
|
7625
8091
|
row._count = bucket;
|
|
7626
8092
|
}
|
|
8093
|
+
}
|
|
8094
|
+
async function loadCounts(params) {
|
|
8095
|
+
const { ctx, model, rows, counts } = params;
|
|
8096
|
+
if (counts.length === 0 || rows.length === 0)
|
|
8097
|
+
return;
|
|
7627
8098
|
for (const node of counts) {
|
|
7628
8099
|
const name = node.name;
|
|
7629
8100
|
const link = node.link;
|
|
@@ -7681,7 +8152,6 @@ async function loadCounts(params) {
|
|
|
7681
8152
|
]
|
|
7682
8153
|
};
|
|
7683
8154
|
};
|
|
7684
|
-
const raw = [];
|
|
7685
8155
|
const chunks = chunkKeys({
|
|
7686
8156
|
ctx,
|
|
7687
8157
|
keys,
|
|
@@ -7690,9 +8160,11 @@ async function loadCounts(params) {
|
|
|
7690
8160
|
measure: node.where !== undefined,
|
|
7691
8161
|
arity: groupFields.length
|
|
7692
8162
|
});
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
8163
|
+
const raw = await executeChunks({
|
|
8164
|
+
ctx,
|
|
8165
|
+
chunks,
|
|
8166
|
+
run: (chunk) => ctx.execute({ statement: build(chunk), model: model.name, method: `_count.${name}` })
|
|
8167
|
+
});
|
|
7696
8168
|
const byKey = new Map;
|
|
7697
8169
|
for (const entry of raw) {
|
|
7698
8170
|
const decoded = groupFields.map((field, index) => decodeValue({
|
|
@@ -7735,15 +8207,7 @@ async function finishNode(params) {
|
|
|
7735
8207
|
applyComputedFields({ model: node.link.target, rows: kept, names: node.args.computed });
|
|
7736
8208
|
}
|
|
7737
8209
|
if (node.args.nested !== null && kept.length > 0) {
|
|
7738
|
-
await
|
|
7739
|
-
dialect: ctx.dialect,
|
|
7740
|
-
meta: ctx.meta,
|
|
7741
|
-
model: node.link.target,
|
|
7742
|
-
rows: [...kept],
|
|
7743
|
-
selection: node.args.nested,
|
|
7744
|
-
execute: ctx.execute,
|
|
7745
|
-
...ctx.wire === undefined ? {} : { wire: ctx.wire }
|
|
7746
|
-
});
|
|
8210
|
+
await attachSelection({ ctx, model: node.link.target, rows: [...kept], selection: node.args.nested });
|
|
7747
8211
|
}
|
|
7748
8212
|
if (node.args.strip.length > 0) {
|
|
7749
8213
|
for (const child of kept) {
|
|
@@ -7753,20 +8217,42 @@ async function finishNode(params) {
|
|
|
7753
8217
|
}
|
|
7754
8218
|
}
|
|
7755
8219
|
async function attachRelations(params) {
|
|
7756
|
-
const { dialect, meta, model, rows, selection, execute, wire } = params;
|
|
8220
|
+
const { dialect, meta, model, rows, selection, execute, wire, concurrency } = params;
|
|
7757
8221
|
if (rows.length === 0)
|
|
7758
8222
|
return;
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
8223
|
+
await attachSelection({
|
|
8224
|
+
ctx: createLoaderContext({ dialect, meta, execute, wire, concurrency }),
|
|
8225
|
+
model,
|
|
8226
|
+
rows,
|
|
8227
|
+
selection
|
|
8228
|
+
});
|
|
8229
|
+
}
|
|
8230
|
+
async function attachSelection(params) {
|
|
8231
|
+
const { ctx, model, rows, selection } = params;
|
|
8232
|
+
if (rows.length === 0)
|
|
8233
|
+
return;
|
|
8234
|
+
if (selection.counts.length > 0)
|
|
8235
|
+
seedCountBuckets({ rows, counts: selection.counts });
|
|
8236
|
+
if (ctx.limiter === null) {
|
|
8237
|
+
for (const node of selection.relations) {
|
|
8238
|
+
await loadNode({ ctx, model, node, rows });
|
|
8239
|
+
}
|
|
8240
|
+
await loadCounts({ ctx, model, rows, counts: selection.counts });
|
|
8241
|
+
return;
|
|
7762
8242
|
}
|
|
7763
|
-
await
|
|
8243
|
+
await runAllSettled({
|
|
8244
|
+
tasks: [
|
|
8245
|
+
...selection.relations.map((node) => () => loadNode({ ctx, model, node, rows })),
|
|
8246
|
+
...selection.counts.map((node) => () => loadCounts({ ctx, model, rows, counts: [node] }))
|
|
8247
|
+
]
|
|
8248
|
+
});
|
|
7764
8249
|
}
|
|
7765
8250
|
async function attachLateralRelations(params) {
|
|
7766
|
-
const { dialect, meta, rows, selection, execute, wire } = params;
|
|
8251
|
+
const { dialect, meta, rows, selection, execute, wire, concurrency } = params;
|
|
7767
8252
|
if (rows.length === 0)
|
|
7768
8253
|
return;
|
|
7769
|
-
const ctx = { dialect, meta, execute, wire };
|
|
8254
|
+
const ctx = createLoaderContext({ dialect, meta, execute, wire, concurrency });
|
|
8255
|
+
const parsed = [];
|
|
7770
8256
|
for (const node of selection.relations) {
|
|
7771
8257
|
const column = lateralColumnAlias({ relation: node.name });
|
|
7772
8258
|
const isList = node.link.relation.isList;
|
|
@@ -7776,12 +8262,12 @@ async function attachLateralRelations(params) {
|
|
|
7776
8262
|
for (const row of rows) {
|
|
7777
8263
|
const raw = row[column];
|
|
7778
8264
|
delete row[column];
|
|
7779
|
-
let
|
|
8265
|
+
let parsed2 = raw;
|
|
7780
8266
|
if (typeof raw === "string") {
|
|
7781
8267
|
try {
|
|
7782
|
-
|
|
8268
|
+
parsed2 = JSON.parse(raw);
|
|
7783
8269
|
} catch (cause) {
|
|
7784
|
-
throw new
|
|
8270
|
+
throw new VibeError17({
|
|
7785
8271
|
code: "VIBE_INTERNAL",
|
|
7786
8272
|
message: `relation column "${column}" returned text that is not valid JSON`,
|
|
7787
8273
|
meta: { relation: node.name },
|
|
@@ -7790,10 +8276,10 @@ async function attachLateralRelations(params) {
|
|
|
7790
8276
|
}
|
|
7791
8277
|
}
|
|
7792
8278
|
if (isList) {
|
|
7793
|
-
if (!Array.isArray(
|
|
7794
|
-
throw new
|
|
8279
|
+
if (!Array.isArray(parsed2)) {
|
|
8280
|
+
throw new VibeError17({ code: "VIBE_INTERNAL", message: "to-many relation payload must be an array", meta: { relation: node.name } });
|
|
7795
8281
|
}
|
|
7796
|
-
const children =
|
|
8282
|
+
const children = parsed2.map(decodePosition);
|
|
7797
8283
|
for (const child of children) {
|
|
7798
8284
|
for (const name of hiddenOrder)
|
|
7799
8285
|
delete child[name];
|
|
@@ -7801,18 +8287,27 @@ async function attachLateralRelations(params) {
|
|
|
7801
8287
|
row[node.name] = children;
|
|
7802
8288
|
pushAll(kept, children);
|
|
7803
8289
|
} else {
|
|
7804
|
-
if (
|
|
8290
|
+
if (parsed2 === null) {
|
|
7805
8291
|
row[node.name] = null;
|
|
7806
8292
|
continue;
|
|
7807
8293
|
}
|
|
7808
|
-
const child = decodePosition(
|
|
8294
|
+
const child = decodePosition(parsed2);
|
|
7809
8295
|
for (const name of hiddenOrder)
|
|
7810
8296
|
delete child[name];
|
|
7811
8297
|
row[node.name] = child;
|
|
7812
8298
|
kept.push(child);
|
|
7813
8299
|
}
|
|
7814
8300
|
}
|
|
7815
|
-
|
|
8301
|
+
parsed.push({ node, kept });
|
|
8302
|
+
}
|
|
8303
|
+
if (ctx.limiter === null) {
|
|
8304
|
+
for (const entry of parsed) {
|
|
8305
|
+
await finishNode({ ctx, node: entry.node, kept: entry.kept });
|
|
8306
|
+
}
|
|
8307
|
+
} else {
|
|
8308
|
+
await runAllSettled({
|
|
8309
|
+
tasks: parsed.map((entry) => () => finishNode({ ctx, node: entry.node, kept: entry.kept }))
|
|
8310
|
+
});
|
|
7816
8311
|
}
|
|
7817
8312
|
if (selection.counts.length === 0)
|
|
7818
8313
|
return;
|
|
@@ -7823,7 +8318,7 @@ async function attachLateralRelations(params) {
|
|
|
7823
8318
|
const raw = row[column];
|
|
7824
8319
|
const count = typeof raw === "number" || typeof raw === "bigint" || typeof raw === "string" && /^\d+$/.test(raw) ? Number(raw) : NaN;
|
|
7825
8320
|
if (!Number.isSafeInteger(count) || count < 0) {
|
|
7826
|
-
throw new
|
|
8321
|
+
throw new VibeError17({ code: "VIBE_INTERNAL", message: "relation count payload must be a non-negative safe integer", meta: { relation: selection.counts[index]?.name } });
|
|
7827
8322
|
}
|
|
7828
8323
|
counts[selection.counts[index].name] = count;
|
|
7829
8324
|
delete row[column];
|
|
@@ -7832,7 +8327,7 @@ async function attachLateralRelations(params) {
|
|
|
7832
8327
|
}
|
|
7833
8328
|
}
|
|
7834
8329
|
// packages/runtime/src/extensions.ts
|
|
7835
|
-
import { VibeError as
|
|
8330
|
+
import { VibeError as VibeError18, extensionManifestEntry } from "@vibeorm/schema";
|
|
7836
8331
|
function assertExtensionManifest(params) {
|
|
7837
8332
|
const { manifest } = params;
|
|
7838
8333
|
const instances = params.instances ?? [];
|
|
@@ -7855,7 +8350,7 @@ function assertExtensionManifest(params) {
|
|
|
7855
8350
|
problems.push(`"${name}" is configured but missing from the generated client`);
|
|
7856
8351
|
}
|
|
7857
8352
|
if (problems.length > 0) {
|
|
7858
|
-
throw new
|
|
8353
|
+
throw new VibeError18({
|
|
7859
8354
|
code: "VIBE_STALE_CLIENT",
|
|
7860
8355
|
message: `extension list changed since generate; run vibeorm generate \u2014 ${problems.join("; ")}`,
|
|
7861
8356
|
meta: { manifest, problems }
|
|
@@ -7870,7 +8365,7 @@ function buildExtensionOperatorMap(params) {
|
|
|
7870
8365
|
for (const operator of extension.sql?.whereOperators ?? []) {
|
|
7871
8366
|
const existing = owner.get(operator.name);
|
|
7872
8367
|
if (existing !== undefined) {
|
|
7873
|
-
throw new
|
|
8368
|
+
throw new VibeError18({
|
|
7874
8369
|
code: "VIBE_CONFIG",
|
|
7875
8370
|
message: `where operator "${operator.name}" is claimed by both "${existing}" and "${extension.name}"`,
|
|
7876
8371
|
meta: { operator: operator.name }
|
|
@@ -7886,7 +8381,7 @@ function buildExtensionOperatorMap(params) {
|
|
|
7886
8381
|
options: extension.options
|
|
7887
8382
|
});
|
|
7888
8383
|
if ("error" in compiled) {
|
|
7889
|
-
throw new
|
|
8384
|
+
throw new VibeError18({
|
|
7890
8385
|
code: "VIBE_VALIDATION",
|
|
7891
8386
|
message: `"${operator.name}" on ${model}.${field}: ${compiled.error}`,
|
|
7892
8387
|
meta: { model, field, operator: operator.name, extension: extension.name }
|
|
@@ -7918,10 +8413,10 @@ function buildConnectHooks(params) {
|
|
|
7918
8413
|
return hooks;
|
|
7919
8414
|
}
|
|
7920
8415
|
// packages/runtime/src/views.ts
|
|
7921
|
-
import { VibeError as
|
|
8416
|
+
import { VibeError as VibeError20 } from "@vibeorm/schema";
|
|
7922
8417
|
|
|
7923
8418
|
// packages/runtime/src/sql-access.ts
|
|
7924
|
-
import { VibeError as
|
|
8419
|
+
import { VibeError as VibeError19 } from "@vibeorm/schema";
|
|
7925
8420
|
import { isSqlFragment as isSqlFragment2 } from "@vibeorm/sql";
|
|
7926
8421
|
function containsSqlFragment(value, seen = new Set) {
|
|
7927
8422
|
if (isSqlFragment2(value))
|
|
@@ -7937,7 +8432,7 @@ function restrictedSqlArgs(params) {
|
|
|
7937
8432
|
const seen = new Map;
|
|
7938
8433
|
const copy = (value) => {
|
|
7939
8434
|
if (isSqlFragment2(value))
|
|
7940
|
-
throw new
|
|
8435
|
+
throw new VibeError19({ code: "VIBE_VALIDATION", message: `SQL fragments are unavailable through ${params.path}; use the unrestricted client`, meta: { path: params.path } });
|
|
7941
8436
|
if (typeof value !== "object" || value === null)
|
|
7942
8437
|
return value;
|
|
7943
8438
|
if (seen.has(value))
|
|
@@ -7964,7 +8459,7 @@ function restrictedSqlArgs(params) {
|
|
|
7964
8459
|
|
|
7965
8460
|
// packages/runtime/src/views.ts
|
|
7966
8461
|
function validationError6(params) {
|
|
7967
|
-
return new
|
|
8462
|
+
return new VibeError20({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
|
|
7968
8463
|
}
|
|
7969
8464
|
|
|
7970
8465
|
class ViewResult {
|
|
@@ -8318,7 +8813,7 @@ function createView(params) {
|
|
|
8318
8813
|
for (const [clientName, mask] of resolved) {
|
|
8319
8814
|
const delegate = models[clientName];
|
|
8320
8815
|
if (delegate === undefined) {
|
|
8321
|
-
throw new
|
|
8816
|
+
throw new VibeError20({
|
|
8322
8817
|
code: "VIBE_INTERNAL",
|
|
8323
8818
|
message: `view "${name}": no delegate for model "${mask.model.name}"`,
|
|
8324
8819
|
meta: { view: name, model: mask.model.name }
|
|
@@ -8334,9 +8829,9 @@ function attachDefineView(params) {
|
|
|
8334
8829
|
Object.defineProperty(client, "$defineView", { value, enumerable: true });
|
|
8335
8830
|
}
|
|
8336
8831
|
// packages/runtime/src/validators.ts
|
|
8337
|
-
import { VibeError as
|
|
8832
|
+
import { VibeError as VibeError21 } from "@vibeorm/schema";
|
|
8338
8833
|
function validationFailed(params) {
|
|
8339
|
-
return new
|
|
8834
|
+
return new VibeError21({
|
|
8340
8835
|
code: "VIBE_VALIDATION",
|
|
8341
8836
|
message: `validation failed for ${params.model}.${params.field}: ${params.issues[0]?.message ?? "invalid value"}`,
|
|
8342
8837
|
meta: { model: params.model, field: params.field, issues: params.issues }
|
|
@@ -8345,7 +8840,7 @@ function validationFailed(params) {
|
|
|
8345
8840
|
function validateValue(params) {
|
|
8346
8841
|
const result = params.schema["~standard"].validate(params.value);
|
|
8347
8842
|
if (result instanceof Promise) {
|
|
8348
|
-
throw new
|
|
8843
|
+
throw new VibeError21({
|
|
8349
8844
|
code: "VIBE_VALIDATION",
|
|
8350
8845
|
message: `async validator on ${params.model}.${params.field} \u2014 write validators must be synchronous (validate before calling the client for async rules)`,
|
|
8351
8846
|
meta: { model: params.model, field: params.field }
|
|
@@ -8468,7 +8963,7 @@ function deferPolicyOperation(params) {
|
|
|
8468
8963
|
}
|
|
8469
8964
|
|
|
8470
8965
|
// packages/runtime/src/policy.ts
|
|
8471
|
-
import { VibeError as
|
|
8966
|
+
import { VibeError as VibeError22 } from "@vibeorm/schema";
|
|
8472
8967
|
|
|
8473
8968
|
// packages/runtime/src/read-only.ts
|
|
8474
8969
|
import { ReadOnlyClientError } from "@vibeorm/schema";
|
|
@@ -8731,7 +9226,7 @@ function inheritReadOnly(params) {
|
|
|
8731
9226
|
|
|
8732
9227
|
// packages/runtime/src/policy.ts
|
|
8733
9228
|
function policyError(message, meta) {
|
|
8734
|
-
return new
|
|
9229
|
+
return new VibeError22({ code: "VIBE_VALIDATION", message, meta: meta ?? {} });
|
|
8735
9230
|
}
|
|
8736
9231
|
function policiedModels(meta) {
|
|
8737
9232
|
return meta.models.filter((model) => model.policy !== undefined);
|
|
@@ -9077,7 +9572,7 @@ function scopedDelegate(params) {
|
|
|
9077
9572
|
const pkWhere = pk.length === 1 ? { [pk[0]]: target[pk[0]] } : { [pkClientKey()]: Object.fromEntries(pk.map((name) => [name, target[name]])) };
|
|
9078
9573
|
return write(raw, pkWhere);
|
|
9079
9574
|
};
|
|
9080
|
-
const notFound2 = (method) => new
|
|
9575
|
+
const notFound2 = (method) => new VibeError22({ code: "VIBE_NOT_FOUND", message: `${method} on ${model.name}: no row matches the unique where inside the bound policy context`, meta: { model: model.name, method } });
|
|
9081
9576
|
return {
|
|
9082
9577
|
findMany: (args) => base.findMany(scopeFind(args)),
|
|
9083
9578
|
findFirst: (args) => base.findFirst(scopeFind(args)),
|
|
@@ -9111,7 +9606,7 @@ function scopedDelegate(params) {
|
|
|
9111
9606
|
})
|
|
9112
9607
|
})),
|
|
9113
9608
|
upsertMany: () => {
|
|
9114
|
-
throw new
|
|
9609
|
+
throw new VibeError22({
|
|
9115
9610
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
9116
9611
|
message: "upsertMany is not available on a policy-bound client: its conflict target is not policy-checked yet",
|
|
9117
9612
|
meta: { model: model.name }
|
|
@@ -9120,7 +9615,7 @@ function scopedDelegate(params) {
|
|
|
9120
9615
|
updateManyAndReturn: (args) => deferred(() => {
|
|
9121
9616
|
const run = base.updateManyAndReturn;
|
|
9122
9617
|
if (run === undefined) {
|
|
9123
|
-
throw new
|
|
9618
|
+
throw new VibeError22({
|
|
9124
9619
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
9125
9620
|
message: "updateManyAndReturn is not available on this delegate",
|
|
9126
9621
|
meta: { model: model.name }
|
|
@@ -9191,7 +9686,7 @@ function globalDelegate(params) {
|
|
|
9191
9686
|
updateManyAndReturn: (args) => deferred(() => {
|
|
9192
9687
|
const run = base.updateManyAndReturn;
|
|
9193
9688
|
if (run === undefined) {
|
|
9194
|
-
throw new
|
|
9689
|
+
throw new VibeError22({
|
|
9195
9690
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
9196
9691
|
message: "updateManyAndReturn is not available on this delegate"
|
|
9197
9692
|
});
|
|
@@ -9202,7 +9697,7 @@ function globalDelegate(params) {
|
|
|
9202
9697
|
upsertMany: (args) => deferred(() => {
|
|
9203
9698
|
if (params.policyBound === false)
|
|
9204
9699
|
return base.upsertMany(args);
|
|
9205
|
-
throw new
|
|
9700
|
+
throw new VibeError22({
|
|
9206
9701
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
9207
9702
|
message: "upsertMany is not available on a policy-bound client: its conflict target is not policy-checked yet"
|
|
9208
9703
|
});
|
|
@@ -9360,7 +9855,7 @@ function attachPolicy(params) {
|
|
|
9360
9855
|
});
|
|
9361
9856
|
}
|
|
9362
9857
|
// packages/runtime/src/scoped.ts
|
|
9363
|
-
import { ScopedQueryError, VibeError as
|
|
9858
|
+
import { ScopedQueryError, VibeError as VibeError23 } from "@vibeorm/schema";
|
|
9364
9859
|
function refusal(params) {
|
|
9365
9860
|
return new ScopedQueryError(params);
|
|
9366
9861
|
}
|
|
@@ -10315,7 +10810,7 @@ function attachScoped(params) {
|
|
|
10315
10810
|
Object.defineProperty(client, "$scoped", {
|
|
10316
10811
|
value: (config) => {
|
|
10317
10812
|
if (!isPlainObject8(config) || !isPlainObject8(config.models)) {
|
|
10318
|
-
throw new
|
|
10813
|
+
throw new VibeError23({
|
|
10319
10814
|
code: "VIBE_VALIDATION",
|
|
10320
10815
|
message: "$scoped takes { scope: string | number | bigint | null, models: { <Model>: \u2026 } }"
|
|
10321
10816
|
});
|
|
@@ -10331,7 +10826,7 @@ function attachScoped(params) {
|
|
|
10331
10826
|
}
|
|
10332
10827
|
const scope = config.scope;
|
|
10333
10828
|
if (scope !== null && typeof scope !== "string" && typeof scope !== "number" && typeof scope !== "bigint") {
|
|
10334
|
-
throw new
|
|
10829
|
+
throw new VibeError23({
|
|
10335
10830
|
code: "VIBE_VALIDATION",
|
|
10336
10831
|
message: `$scoped: scope must be a string, number, bigint or null \u2014 got ${typeof scope}`
|
|
10337
10832
|
});
|
|
@@ -10347,7 +10842,7 @@ function attachScoped(params) {
|
|
|
10347
10842
|
});
|
|
10348
10843
|
}
|
|
10349
10844
|
// packages/runtime/src/rls-readiness.ts
|
|
10350
|
-
import { VibeError as
|
|
10845
|
+
import { VibeError as VibeError24, assertValidSchemaIR, unsupportedCapability as unsupportedCapability7 } from "@vibeorm/schema";
|
|
10351
10846
|
import { NATIVE_RLS as NATIVE_RLS2, getSqlDialect as getSqlDialect4, renderRlsPolicy, renderRlsRole, NATIVE_RLS_PROBE_POLICY, renderRlsReadinessQuery, renderRlsProbeTable } from "@vibeorm/sql";
|
|
10352
10847
|
import { RLS_READINESS_SQL, NATIVE_RLS_PROBE_POLICY as NATIVE_RLS_PROBE_POLICY2 } from "@vibeorm/sql";
|
|
10353
10848
|
function textOf(params) {
|
|
@@ -10370,6 +10865,35 @@ function intOf(params) {
|
|
|
10370
10865
|
return null;
|
|
10371
10866
|
return Number.parseInt(value, 10);
|
|
10372
10867
|
}
|
|
10868
|
+
function unreadableCatalog(params) {
|
|
10869
|
+
return new VibeError24({
|
|
10870
|
+
code: "VIBE_ADAPTER",
|
|
10871
|
+
message: `native RLS readiness could not read the "${params.key}" result of the folded catalog read: ${params.detail}. ` + "The inspection is refused rather than treated as an empty result set.",
|
|
10872
|
+
meta: { result: params.key }
|
|
10873
|
+
});
|
|
10874
|
+
}
|
|
10875
|
+
function catalogRowsOf(params) {
|
|
10876
|
+
const value = params.row[params.key];
|
|
10877
|
+
if (value === null || value === undefined)
|
|
10878
|
+
return [];
|
|
10879
|
+
let parsed = value;
|
|
10880
|
+
if (typeof value === "string") {
|
|
10881
|
+
try {
|
|
10882
|
+
parsed = JSON.parse(value);
|
|
10883
|
+
} catch (error) {
|
|
10884
|
+
throw unreadableCatalog({ key: params.key, detail: describeFailure({ error }) });
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10887
|
+
if (!Array.isArray(parsed)) {
|
|
10888
|
+
throw unreadableCatalog({ key: params.key, detail: `expected a JSON array, got ${typeof parsed}` });
|
|
10889
|
+
}
|
|
10890
|
+
return parsed.map((entry) => {
|
|
10891
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
10892
|
+
throw unreadableCatalog({ key: params.key, detail: `expected JSON objects, got ${entry === null ? "null" : typeof entry}` });
|
|
10893
|
+
}
|
|
10894
|
+
return entry;
|
|
10895
|
+
});
|
|
10896
|
+
}
|
|
10373
10897
|
var EXPRESSION_CACHE_LIMITS = Object.freeze({ connections: 64 });
|
|
10374
10898
|
var expressionCaches = new WeakMap;
|
|
10375
10899
|
function buildCheck(params) {
|
|
@@ -10401,7 +10925,7 @@ function requireNativeRls(params) {
|
|
|
10401
10925
|
throw unsupportedCapability7({ dialect: dialect.name, feature: "nativeRowLevelSecurity" });
|
|
10402
10926
|
}
|
|
10403
10927
|
if (params.schema.rls === undefined) {
|
|
10404
|
-
throw new
|
|
10928
|
+
throw new VibeError24({
|
|
10405
10929
|
code: "VIBE_CONFIG",
|
|
10406
10930
|
message: "Native row-level security readiness requires a schema that declares native RLS"
|
|
10407
10931
|
});
|
|
@@ -10692,8 +11216,9 @@ async function inspectNativeRlsTransaction(params) {
|
|
|
10692
11216
|
const dialect = requireNativeRls({ adapter, schema });
|
|
10693
11217
|
const protectedTables = protectedTablesOf({ schema });
|
|
10694
11218
|
const unprotected = unprotectedTablesOf({ schema });
|
|
10695
|
-
const
|
|
10696
|
-
const
|
|
11219
|
+
const catalogRows = await adapter.execute(renderRlsReadinessQuery({ dialect, query: "catalog" }));
|
|
11220
|
+
const catalog = catalogRows[0] ?? {};
|
|
11221
|
+
const identityRow = catalogRowsOf({ row: catalog, key: "identity" })[0] ?? {};
|
|
10697
11222
|
const identity = {
|
|
10698
11223
|
currentRole: textOf({ row: identityRow, key: "currentUser" }),
|
|
10699
11224
|
sessionRole: textOf({ row: identityRow, key: "sessionUser" }),
|
|
@@ -10703,13 +11228,13 @@ async function inspectNativeRlsTransaction(params) {
|
|
|
10703
11228
|
createRole: flagOf({ row: identityRow, key: "createRole" }),
|
|
10704
11229
|
serverVersionNum: intOf({ row: identityRow, key: "serverVersionNum" })
|
|
10705
11230
|
};
|
|
10706
|
-
const elevatedRows =
|
|
10707
|
-
const controlRows =
|
|
10708
|
-
const tableRows =
|
|
10709
|
-
const policyRows =
|
|
10710
|
-
const viewRows =
|
|
10711
|
-
const routineRows =
|
|
10712
|
-
const triggerRows =
|
|
11231
|
+
const elevatedRows = catalogRowsOf({ row: catalog, key: "elevatedRoles" });
|
|
11232
|
+
const controlRows = catalogRowsOf({ row: catalog, key: "schemaControl" });
|
|
11233
|
+
const tableRows = catalogRowsOf({ row: catalog, key: "tables" });
|
|
11234
|
+
const policyRows = catalogRowsOf({ row: catalog, key: "policies" });
|
|
11235
|
+
const viewRows = catalogRowsOf({ row: catalog, key: "views" });
|
|
11236
|
+
const routineRows = catalogRowsOf({ row: catalog, key: "routines" });
|
|
11237
|
+
const triggerRows = catalogRowsOf({ row: catalog, key: "triggers" });
|
|
10713
11238
|
const controlRow = controlRows[0];
|
|
10714
11239
|
const control = controlRow === undefined ? undefined : {
|
|
10715
11240
|
schema: textOf({ row: controlRow, key: "schema" }),
|
|
@@ -10813,7 +11338,7 @@ async function inspectNativeRls(params) {
|
|
|
10813
11338
|
function requireEmbeddedRole(params) {
|
|
10814
11339
|
const { role } = params;
|
|
10815
11340
|
if (typeof role !== "string" || role.length === 0 || role.includes("\x00")) {
|
|
10816
|
-
throw new
|
|
11341
|
+
throw new VibeError24({
|
|
10817
11342
|
code: "VIBE_CONFIG",
|
|
10818
11343
|
message: "PGlite native row-level security requires a pre-existing restricted runtime role (nativeRls.runtimeRole) \u2014 its default embedded session is privileged and bypasses every policy",
|
|
10819
11344
|
meta: { provider: "pglite" }
|
|
@@ -10825,7 +11350,7 @@ function assertNativeRlsReady(params) {
|
|
|
10825
11350
|
const { report } = params;
|
|
10826
11351
|
if (report.ready)
|
|
10827
11352
|
return;
|
|
10828
|
-
throw new
|
|
11353
|
+
throw new VibeError24({
|
|
10829
11354
|
code: "VIBE_CONFIG",
|
|
10830
11355
|
message: `Native row-level security is not ready on this database: ${report.problems.map((problem) => `${problem.name} (${problem.status})`).join(", ")}`,
|
|
10831
11356
|
meta: {
|
|
@@ -10851,7 +11376,7 @@ function formatNativeRlsReport(params) {
|
|
|
10851
11376
|
}
|
|
10852
11377
|
// packages/runtime/src/client.ts
|
|
10853
11378
|
import { compileSql } from "@vibeorm/sql";
|
|
10854
|
-
import { RlsContextError as RlsContextError2, VibeError as
|
|
11379
|
+
import { RlsContextError as RlsContextError2, VibeError as VibeError28, assertValidSchemaIR as assertValidSchemaIR2, unsupportedCapability as unsupportedCapability8 } from "@vibeorm/schema";
|
|
10855
11380
|
import { ADVISORY_LOCK_RESULT_COLUMN, ParamCollector, getSqlDialect as getSqlDialect5, renderRlsContext, renderRlsPolicy as renderRlsPolicy2, renderRlsRole as renderRlsRole2, renderStatement as renderStatement5 } from "@vibeorm/sql";
|
|
10856
11381
|
|
|
10857
11382
|
// packages/runtime/src/render-cache.ts
|
|
@@ -11362,11 +11887,11 @@ function renderStatementCached(params) {
|
|
|
11362
11887
|
}
|
|
11363
11888
|
|
|
11364
11889
|
// packages/runtime/src/strict-args.ts
|
|
11365
|
-
import { VibeError as
|
|
11890
|
+
import { VibeError as VibeError25 } from "@vibeorm/schema";
|
|
11366
11891
|
var SKIP = Symbol.for("vibeorm.skip");
|
|
11367
11892
|
function explicitUndefined(params) {
|
|
11368
11893
|
const { model, method, path } = params;
|
|
11369
|
-
return new
|
|
11894
|
+
return new VibeError25({
|
|
11370
11895
|
code: "VIBE_VALIDATION",
|
|
11371
11896
|
message: `${model}.${method}(): explicit undefined at ${path} \u2014 an undefined value is refused ` + `because it would silently drop the property (widening a filter or losing a write). ` + `Omit the property, or pass SKIP to omit it deliberately; ` + `VibeClient({ strictArguments: false }) restores the legacy behaviour.`,
|
|
11372
11897
|
meta: { model, method, path }
|
|
@@ -11543,10 +12068,10 @@ function bindRlsContext(params) {
|
|
|
11543
12068
|
}
|
|
11544
12069
|
|
|
11545
12070
|
// packages/runtime/src/telemetry/recorder.ts
|
|
11546
|
-
import { VibeError as
|
|
12071
|
+
import { VibeError as VibeError27 } from "@vibeorm/schema";
|
|
11547
12072
|
|
|
11548
12073
|
// packages/runtime/src/telemetry/config.ts
|
|
11549
|
-
import { VibeError as
|
|
12074
|
+
import { VibeError as VibeError26 } from "@vibeorm/schema";
|
|
11550
12075
|
var TELEMETRY_DEFAULTS = Object.freeze({
|
|
11551
12076
|
captureSql: false,
|
|
11552
12077
|
captureParameterValues: false,
|
|
@@ -11560,8 +12085,8 @@ var TELEMETRY_DEFAULTS = Object.freeze({
|
|
|
11560
12085
|
maxFingerprintLength: 512,
|
|
11561
12086
|
latencyBucketsMs: Object.freeze([1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000])
|
|
11562
12087
|
});
|
|
11563
|
-
function
|
|
11564
|
-
return new
|
|
12088
|
+
function configError2(params) {
|
|
12089
|
+
return new VibeError26({
|
|
11565
12090
|
code: "VIBE_CONFIG",
|
|
11566
12091
|
message: `telemetry: ${params.message}`,
|
|
11567
12092
|
...params.meta === undefined ? {} : { meta: params.meta }
|
|
@@ -11572,7 +12097,7 @@ function positiveInteger(params) {
|
|
|
11572
12097
|
if (value === undefined)
|
|
11573
12098
|
return fallback;
|
|
11574
12099
|
if (!Number.isInteger(value) || value < 1) {
|
|
11575
|
-
throw
|
|
12100
|
+
throw configError2({ message: `${name} must be a positive integer, received ${String(value)}`, meta: { option: name } });
|
|
11576
12101
|
}
|
|
11577
12102
|
return value;
|
|
11578
12103
|
}
|
|
@@ -11581,22 +12106,22 @@ function nonNegativeNumber(params) {
|
|
|
11581
12106
|
if (value === undefined)
|
|
11582
12107
|
return fallback;
|
|
11583
12108
|
if (!Number.isFinite(value) || value < 0) {
|
|
11584
|
-
throw
|
|
12109
|
+
throw configError2({ message: `${name} must be a finite number >= 0, received ${String(value)}`, meta: { option: name } });
|
|
11585
12110
|
}
|
|
11586
12111
|
return value;
|
|
11587
12112
|
}
|
|
11588
12113
|
function resolveTelemetryConfig(params) {
|
|
11589
12114
|
const { options } = params;
|
|
11590
12115
|
if (options.exporter === undefined || options.exporter === null || typeof options.exporter !== "object") {
|
|
11591
|
-
throw
|
|
12116
|
+
throw configError2({ message: "an exporter is required \u2014 telemetry has nowhere to send events without one" });
|
|
11592
12117
|
}
|
|
11593
12118
|
const sampleRate = options.sampleRate ?? TELEMETRY_DEFAULTS.sampleRate;
|
|
11594
12119
|
if (!Number.isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1) {
|
|
11595
|
-
throw
|
|
12120
|
+
throw configError2({ message: `sampleRate must be between 0 and 1, received ${String(sampleRate)}`, meta: { option: "sampleRate" } });
|
|
11596
12121
|
}
|
|
11597
12122
|
const delivery = options.delivery ?? TELEMETRY_DEFAULTS.delivery;
|
|
11598
12123
|
if (delivery !== "immediate" && delivery !== "buffered") {
|
|
11599
|
-
throw
|
|
12124
|
+
throw configError2({ message: `delivery must be "immediate" or "buffered", received ${JSON.stringify(delivery)}`, meta: { option: "delivery" } });
|
|
11600
12125
|
}
|
|
11601
12126
|
return {
|
|
11602
12127
|
captureSql: options.captureSql ?? TELEMETRY_DEFAULTS.captureSql,
|
|
@@ -11649,7 +12174,7 @@ function deriveStage(params) {
|
|
|
11649
12174
|
return "decode";
|
|
11650
12175
|
}
|
|
11651
12176
|
function errorCodeOf(error) {
|
|
11652
|
-
return error instanceof
|
|
12177
|
+
return error instanceof VibeError27 ? error.code : undefined;
|
|
11653
12178
|
}
|
|
11654
12179
|
function newHistogram(bounds) {
|
|
11655
12180
|
return { counts: new Array(bounds.length + 1).fill(0), sum: 0, count: 0 };
|
|
@@ -11968,8 +12493,8 @@ function createTelemetryRecorder(params) {
|
|
|
11968
12493
|
|
|
11969
12494
|
// packages/runtime/src/telemetry/statement.ts
|
|
11970
12495
|
async function instrumentStatement(params) {
|
|
11971
|
-
const { scope, legacy, run } = params;
|
|
11972
|
-
if (scope === undefined && legacy === undefined)
|
|
12496
|
+
const { scope, legacy, diagnose, run } = params;
|
|
12497
|
+
if (scope === undefined && legacy === undefined && diagnose === undefined)
|
|
11973
12498
|
return await run();
|
|
11974
12499
|
const started = performance.now();
|
|
11975
12500
|
let value;
|
|
@@ -12017,13 +12542,22 @@ async function instrumentStatement(params) {
|
|
|
12017
12542
|
};
|
|
12018
12543
|
scope.statement(success);
|
|
12019
12544
|
}
|
|
12545
|
+
diagnose?.({
|
|
12546
|
+
model: params.model,
|
|
12547
|
+
method: params.method,
|
|
12548
|
+
text: params.text,
|
|
12549
|
+
values: params.values,
|
|
12550
|
+
durationMs,
|
|
12551
|
+
inTransaction: params.inTransaction,
|
|
12552
|
+
raw: params.kind === "raw"
|
|
12553
|
+
});
|
|
12020
12554
|
return value;
|
|
12021
12555
|
}
|
|
12022
12556
|
|
|
12023
12557
|
// packages/runtime/src/client.ts
|
|
12024
12558
|
function notFound2(params) {
|
|
12025
12559
|
const { model, method } = params;
|
|
12026
|
-
return new
|
|
12560
|
+
return new VibeError28({
|
|
12027
12561
|
code: "VIBE_NOT_FOUND",
|
|
12028
12562
|
message: `no ${model} record matched the where clause of ${method}()`,
|
|
12029
12563
|
meta: { model, method }
|
|
@@ -12031,7 +12565,7 @@ function notFound2(params) {
|
|
|
12031
12565
|
}
|
|
12032
12566
|
function transactionLifecycleRefusal(params) {
|
|
12033
12567
|
const { method } = params;
|
|
12034
|
-
return new
|
|
12568
|
+
return new VibeError28({
|
|
12035
12569
|
code: "VIBE_VALIDATION",
|
|
12036
12570
|
message: `${method}() is not available on a transaction client \u2014 the connection is owned by the open transaction; call it on the client that started the transaction`,
|
|
12037
12571
|
meta: { method, inTransaction: true }
|
|
@@ -12039,7 +12573,7 @@ function transactionLifecycleRefusal(params) {
|
|
|
12039
12573
|
}
|
|
12040
12574
|
function viewIsReadOnly(params) {
|
|
12041
12575
|
const { model, method } = params;
|
|
12042
|
-
return new
|
|
12576
|
+
return new VibeError28({
|
|
12043
12577
|
code: "VIBE_VALIDATION",
|
|
12044
12578
|
message: `${model} is a view \u2014 ${method}() is not available on views (they are read-only); write to the underlying table instead`,
|
|
12045
12579
|
meta: { model, method }
|
|
@@ -12145,7 +12679,7 @@ async function engineTransaction(params) {
|
|
|
12145
12679
|
const support = engine.adapter.budgets?.transactionDeadline;
|
|
12146
12680
|
const requested = options.deadline.enforcement ?? "between-statements";
|
|
12147
12681
|
if (support === undefined || support === "unsupported" || requested === "cancel-running-statements" && support !== "cancel-running-statements") {
|
|
12148
|
-
throw new
|
|
12682
|
+
throw new VibeError28({
|
|
12149
12683
|
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
12150
12684
|
message: "the adapter does not declare support for the requested transaction deadline",
|
|
12151
12685
|
meta: { provider: engine.adapter.provider, feature: "transactionDeadline", requested, supported: support ?? "undeclared" }
|
|
@@ -12221,6 +12755,7 @@ function runStatement(params) {
|
|
|
12221
12755
|
return instrumentStatement({
|
|
12222
12756
|
scope: engine.operation,
|
|
12223
12757
|
legacy: legacySink({ engine }),
|
|
12758
|
+
diagnose: engine.diagnostics?.observe,
|
|
12224
12759
|
kind: params.kind,
|
|
12225
12760
|
model: params.model,
|
|
12226
12761
|
method: params.method,
|
|
@@ -12335,7 +12870,7 @@ class QueryPromise extends Promise {
|
|
|
12335
12870
|
}
|
|
12336
12871
|
#claim() {
|
|
12337
12872
|
if (this.#started) {
|
|
12338
|
-
throw new
|
|
12873
|
+
throw new VibeError28({
|
|
12339
12874
|
code: "VIBE_VALIDATION",
|
|
12340
12875
|
message: "$transaction([...]) needs operations that have not dispatched \u2014 pass delegate calls in the same tick they are created (this one was awaited or created earlier and already executed outside the transaction)"
|
|
12341
12876
|
});
|
|
@@ -12453,14 +12988,14 @@ function plan(params) {
|
|
|
12453
12988
|
throw readOnlyRefusal({ path: `${model}.${method}`, reason: "raw" });
|
|
12454
12989
|
if (args !== undefined && args["lock"] !== undefined) {
|
|
12455
12990
|
if (engine.inTransaction !== true) {
|
|
12456
|
-
throw new
|
|
12991
|
+
throw new VibeError28({
|
|
12457
12992
|
code: "VIBE_VALIDATION",
|
|
12458
12993
|
message: `lock on ${model}.${method}() requires a transaction \u2014 call it on the client $transaction hands its callback (outside one, the lock would release at statement end)`,
|
|
12459
12994
|
meta: { model, method }
|
|
12460
12995
|
});
|
|
12461
12996
|
}
|
|
12462
12997
|
if (!LOCKABLE_METHODS.includes(method)) {
|
|
12463
|
-
throw new
|
|
12998
|
+
throw new VibeError28({
|
|
12464
12999
|
code: "VIBE_VALIDATION",
|
|
12465
13000
|
message: `lock on ${model}.${method}() is not supported \u2014 only ${LOCKABLE_METHODS.join(", ")} take a lock. ` + `Lock the rows with a find inside the transaction, then write them`,
|
|
12466
13001
|
meta: { model, method }
|
|
@@ -12487,7 +13022,7 @@ function readAcquiredVerdict(params) {
|
|
|
12487
13022
|
return true;
|
|
12488
13023
|
if (value === false || value === 0 || value === "f" || value === "false" || value === "0")
|
|
12489
13024
|
return false;
|
|
12490
|
-
throw new
|
|
13025
|
+
throw new VibeError28({
|
|
12491
13026
|
code: "VIBE_INTERNAL",
|
|
12492
13027
|
message: `internal: ${params.method}() expected a boolean "${ADVISORY_LOCK_RESULT_COLUMN}" column, got ${String(value)}`,
|
|
12493
13028
|
meta: { method: params.method, value: String(value) }
|
|
@@ -12547,7 +13082,7 @@ async function insertedKeyWhere(params) {
|
|
|
12547
13082
|
});
|
|
12548
13083
|
const id = result.rows[0]?.id;
|
|
12549
13084
|
if (id === undefined || id === null) {
|
|
12550
|
-
throw new
|
|
13085
|
+
throw new VibeError28({
|
|
12551
13086
|
code: "VIBE_INTERNAL",
|
|
12552
13087
|
message: `could not read back the generated key for ${source.model.name}`,
|
|
12553
13088
|
meta: { model: source.model.name }
|
|
@@ -12723,10 +13258,17 @@ function relationExecutor(params) {
|
|
|
12723
13258
|
});
|
|
12724
13259
|
};
|
|
12725
13260
|
}
|
|
13261
|
+
function relationConcurrency(params) {
|
|
13262
|
+
const { engine } = params;
|
|
13263
|
+
if (engine.inTransaction === true)
|
|
13264
|
+
return 1;
|
|
13265
|
+
return engine.options.relationLoading?.concurrency ?? 1;
|
|
13266
|
+
}
|
|
12726
13267
|
async function finalizeRows(params) {
|
|
12727
13268
|
const { engine, model, shape, rows } = params;
|
|
12728
13269
|
if (shape.relations !== null && rows.length > 0) {
|
|
12729
13270
|
const execute = relationExecutor({ engine });
|
|
13271
|
+
const concurrency = relationConcurrency({ engine });
|
|
12730
13272
|
if (shape.relationStrategy === "join") {
|
|
12731
13273
|
await attachLateralRelations({
|
|
12732
13274
|
dialect: engine.dialect,
|
|
@@ -12735,7 +13277,8 @@ async function finalizeRows(params) {
|
|
|
12735
13277
|
rows,
|
|
12736
13278
|
selection: shape.relations,
|
|
12737
13279
|
execute,
|
|
12738
|
-
wire: engine.adapter.wire
|
|
13280
|
+
wire: engine.adapter.wire,
|
|
13281
|
+
concurrency
|
|
12739
13282
|
});
|
|
12740
13283
|
} else {
|
|
12741
13284
|
await attachRelations({
|
|
@@ -12745,7 +13288,8 @@ async function finalizeRows(params) {
|
|
|
12745
13288
|
rows,
|
|
12746
13289
|
selection: shape.relations,
|
|
12747
13290
|
execute,
|
|
12748
|
-
wire: engine.adapter.wire
|
|
13291
|
+
wire: engine.adapter.wire,
|
|
13292
|
+
concurrency
|
|
12749
13293
|
});
|
|
12750
13294
|
}
|
|
12751
13295
|
}
|
|
@@ -12866,7 +13410,7 @@ function createDelegate(params) {
|
|
|
12866
13410
|
const skipping = args.onConflict !== undefined;
|
|
12867
13411
|
if (hasNestedWrites({ model: modelMeta, data: args.data })) {
|
|
12868
13412
|
if (skipping) {
|
|
12869
|
-
throw new
|
|
13413
|
+
throw new VibeError28({
|
|
12870
13414
|
code: "VIBE_VALIDATION",
|
|
12871
13415
|
message: `onConflict cannot combine with nested writes on ${modelMeta.name}.create() \u2014 insert the parent row alone, then connect relations`,
|
|
12872
13416
|
meta: { model: modelMeta.name }
|
|
@@ -12967,7 +13511,7 @@ function createDelegate(params) {
|
|
|
12967
13511
|
const upsert = async (target, args) => {
|
|
12968
13512
|
const modelMeta = getModelMeta({ meta: target.meta, model });
|
|
12969
13513
|
if (hasNestedWrites({ model: modelMeta, data: args.create }) || hasNestedWrites({ model: modelMeta, data: args.update })) {
|
|
12970
|
-
throw new
|
|
13514
|
+
throw new VibeError28({
|
|
12971
13515
|
code: "VIBE_VALIDATION",
|
|
12972
13516
|
message: `nested writes are not supported inside upsert arms on ${modelMeta.name} \u2014 use create()/update() with nested data`,
|
|
12973
13517
|
meta: { model: modelMeta.name }
|
|
@@ -12975,7 +13519,7 @@ function createDelegate(params) {
|
|
|
12975
13519
|
}
|
|
12976
13520
|
if (!upsertUsesNativeStatement({ model: modelMeta, args })) {
|
|
12977
13521
|
if (args.onConflict !== undefined) {
|
|
12978
|
-
throw new
|
|
13522
|
+
throw new VibeError28({
|
|
12979
13523
|
code: "VIBE_VALIDATION",
|
|
12980
13524
|
message: `onConflict on ${modelMeta.name}.upsert() needs the native single-statement path \u2014 this shape (a guard field beyond the conflict target, or create data not binding the where's key) runs the find-then-write fallback, which cannot honor an explicit conflict target`,
|
|
12981
13525
|
meta: { model: modelMeta.name }
|
|
@@ -13253,7 +13797,7 @@ function buildClient(params) {
|
|
|
13253
13797
|
}
|
|
13254
13798
|
for (const operation of operations) {
|
|
13255
13799
|
if (!isQueryPromise(operation)) {
|
|
13256
|
-
claimError = new
|
|
13800
|
+
claimError = new VibeError28({
|
|
13257
13801
|
code: "VIBE_VALIDATION",
|
|
13258
13802
|
message: "$transaction([...]) takes query promises from model delegate calls \u2014 a foreign promise is already running and cannot join the transaction"
|
|
13259
13803
|
});
|
|
@@ -13318,7 +13862,7 @@ function buildClient(params) {
|
|
|
13318
13862
|
$models: models,
|
|
13319
13863
|
$on: (event, listener) => {
|
|
13320
13864
|
if (event !== "query") {
|
|
13321
|
-
throw new
|
|
13865
|
+
throw new VibeError28({
|
|
13322
13866
|
code: "VIBE_VALIDATION",
|
|
13323
13867
|
message: `unknown client event "${String(event)}" \u2014 supported: "query"`,
|
|
13324
13868
|
meta: { event }
|
|
@@ -13333,7 +13877,7 @@ function buildClient(params) {
|
|
|
13333
13877
|
$querySql: async ({ query, decoder }) => {
|
|
13334
13878
|
assertEngineActive({ engine });
|
|
13335
13879
|
if (decoder === null || typeof decoder !== "object" || typeof decoder.decode !== "function")
|
|
13336
|
-
throw new
|
|
13880
|
+
throw new VibeError28({ code: "VIBE_VALIDATION", message: "$querySql requires an explicit result decoder" });
|
|
13337
13881
|
const statement = compileSql({ dialect: engine.dialect, query });
|
|
13338
13882
|
let output = [];
|
|
13339
13883
|
await runRaw("$querySql", statement, (rows) => {
|
|
@@ -13377,6 +13921,7 @@ function buildClient(params) {
|
|
|
13377
13921
|
assertEngineActive({ engine, requireContext: false });
|
|
13378
13922
|
if (engine.inTransaction === true)
|
|
13379
13923
|
throw transactionLifecycleRefusal({ method: "$disconnect" });
|
|
13924
|
+
await engine.diagnostics?.flush();
|
|
13380
13925
|
await engine.adapter.disconnect();
|
|
13381
13926
|
await engine.telemetry?.flush();
|
|
13382
13927
|
}
|
|
@@ -13409,7 +13954,7 @@ function buildClient(params) {
|
|
|
13409
13954
|
});
|
|
13410
13955
|
} else {
|
|
13411
13956
|
const refuseLegacy = () => {
|
|
13412
|
-
throw new
|
|
13957
|
+
throw new VibeError28({ code: "VIBE_CONFIG", message: "Native row-level security cannot be combined with application policy or tenant scoping" });
|
|
13413
13958
|
};
|
|
13414
13959
|
Object.defineProperties(client, {
|
|
13415
13960
|
$withContext: { enumerable: true, value: (context) => {
|
|
@@ -13479,7 +14024,7 @@ function makeMountContext(params) {
|
|
|
13479
14024
|
compileWhere: ({ where, model: modelName, qualify }) => {
|
|
13480
14025
|
const target = modelName === undefined ? model : getModelMeta({ meta: engine.meta, model: modelName });
|
|
13481
14026
|
if (target === null) {
|
|
13482
|
-
throw new
|
|
14027
|
+
throw new VibeError28({
|
|
13483
14028
|
code: "VIBE_INTERNAL",
|
|
13484
14029
|
message: `extension "${extension}" compiled a where with no model in scope`,
|
|
13485
14030
|
meta: { extension }
|
|
@@ -13508,24 +14053,24 @@ function createMountSurface(params) {
|
|
|
13508
14053
|
for (const key of path) {
|
|
13509
14054
|
receiver = current;
|
|
13510
14055
|
if (current === null || typeof current !== "object" && typeof current !== "function")
|
|
13511
|
-
throw new
|
|
14056
|
+
throw new VibeError28({ code: "VIBE_CONFIG", message: "Extension mount changed its method shape" });
|
|
13512
14057
|
current = Reflect.get(current, key);
|
|
13513
14058
|
}
|
|
13514
14059
|
if (typeof current !== "function")
|
|
13515
|
-
throw new
|
|
14060
|
+
throw new VibeError28({ code: "VIBE_CONFIG", message: "Extension mount changed its method shape" });
|
|
13516
14061
|
return Reflect.apply(current, receiver, args);
|
|
13517
14062
|
} });
|
|
13518
14063
|
}
|
|
13519
14064
|
if (value === null || typeof value !== "object")
|
|
13520
14065
|
return value;
|
|
13521
14066
|
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
13522
|
-
throw new
|
|
14067
|
+
throw new VibeError28({ code: "VIBE_CONFIG", message: "Native RLS extension mounts must expose plain objects and methods" });
|
|
13523
14068
|
}
|
|
13524
14069
|
const result = {};
|
|
13525
14070
|
for (const key of Reflect.ownKeys(value)) {
|
|
13526
14071
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
13527
14072
|
if (!("value" in descriptor))
|
|
13528
|
-
throw new
|
|
14073
|
+
throw new VibeError28({ code: "VIBE_CONFIG", message: "Native RLS extension mount accessors are unsupported" });
|
|
13529
14074
|
Object.defineProperty(result, key, { value: wrap(descriptor.value, [...path, key]), enumerable: descriptor.enumerable });
|
|
13530
14075
|
}
|
|
13531
14076
|
return Object.freeze(result);
|
|
@@ -13542,7 +14087,7 @@ function attachExtensionMounts(params) {
|
|
|
13542
14087
|
const model = getModelMeta({ meta: engine.meta, model: mount.model ?? "" });
|
|
13543
14088
|
const delegate = models[model.name];
|
|
13544
14089
|
if (delegate === undefined || mount.property in delegate) {
|
|
13545
|
-
throw new
|
|
14090
|
+
throw new VibeError28({
|
|
13546
14091
|
code: "VIBE_CONFIG",
|
|
13547
14092
|
message: `extension "${mount.extension}" cannot mount "${mount.property}" on ${model.name} \u2014 the name is taken`,
|
|
13548
14093
|
meta: { extension: mount.extension, model: model.name, property: mount.property }
|
|
@@ -13553,7 +14098,7 @@ function attachExtensionMounts(params) {
|
|
|
13553
14098
|
continue;
|
|
13554
14099
|
}
|
|
13555
14100
|
if (!mount.property.startsWith("$") || mount.property in client) {
|
|
13556
|
-
throw new
|
|
14101
|
+
throw new VibeError28({
|
|
13557
14102
|
code: "VIBE_CONFIG",
|
|
13558
14103
|
message: `extension "${mount.extension}" cannot mount client property "${mount.property}" \u2014 root mounts are $-prefixed and must not collide`,
|
|
13559
14104
|
meta: { extension: mount.extension, property: mount.property }
|
|
@@ -13567,6 +14112,15 @@ function createClient(params) {
|
|
|
13567
14112
|
const { schema, adapter, options, extensions, computed } = params;
|
|
13568
14113
|
const dialectName = adapter.dialect;
|
|
13569
14114
|
const telemetry = options?.telemetry === undefined ? undefined : createTelemetryRecorder({ options: options.telemetry });
|
|
14115
|
+
resolveRelationConcurrency({ concurrency: options?.relationLoading?.concurrency });
|
|
14116
|
+
const diagnosticsConfig = resolveStatementDiagnostics({
|
|
14117
|
+
options: options?.diagnostics,
|
|
14118
|
+
env: process.env[STATEMENT_DIAGNOSTICS_DEFAULTS.envVariable]
|
|
14119
|
+
});
|
|
14120
|
+
const diagnostics = diagnosticsConfig === undefined ? undefined : createStatementDiagnostics({ config: diagnosticsConfig, adapter, dialect: getSqlDialect5({ dialect: dialectName }) });
|
|
14121
|
+
if (diagnosticsConfig !== undefined && diagnosticsConfig.explain === "analyze" && diagnosticsConfig.slowerThanMs < STATEMENT_DIAGNOSTICS_DEFAULTS.analyzeWarnBelowMs) {
|
|
14122
|
+
console.warn(`[vibeorm] diagnostics: explain "analyze" with slowerThanMs ${diagnosticsConfig.slowerThanMs} re-executes every read statement under EXPLAIN ANALYZE \u2014 set a threshold, and never use this against production`);
|
|
14123
|
+
}
|
|
13570
14124
|
if (schema.rls !== undefined) {
|
|
13571
14125
|
const dialect = getSqlDialect5({ dialect: dialectName });
|
|
13572
14126
|
if (!dialect.capabilities.nativeRowLevelSecurity)
|
|
@@ -13579,9 +14133,9 @@ function createClient(params) {
|
|
|
13579
14133
|
const role = options?.nativeRls?.runtimeRole;
|
|
13580
14134
|
if (adapter.provider === "pglite") {
|
|
13581
14135
|
if (typeof role !== "string" || role.length === 0 || role.includes("\x00"))
|
|
13582
|
-
throw new
|
|
14136
|
+
throw new VibeError28({ code: "VIBE_CONFIG", message: "PGlite native row-level security requires a pre-existing restricted runtimeRole; embedded binding is not a boundary against the host process" });
|
|
13583
14137
|
} else if (role !== undefined) {
|
|
13584
|
-
throw new
|
|
14138
|
+
throw new VibeError28({ code: "VIBE_CONFIG", message: "Server native row-level security must use the runtime login role, not a runtimeRole switch" });
|
|
13585
14139
|
}
|
|
13586
14140
|
}
|
|
13587
14141
|
if (extensions?.manifest !== undefined) {
|
|
@@ -13591,7 +14145,7 @@ function createClient(params) {
|
|
|
13591
14145
|
assertComputedManifest({ manifest: computed.manifest, specs: computed.specs });
|
|
13592
14146
|
}
|
|
13593
14147
|
if (computed === undefined && options !== undefined && options["computed"] !== undefined) {
|
|
13594
|
-
throw new
|
|
14148
|
+
throw new VibeError28({
|
|
13595
14149
|
code: "VIBE_CONFIG",
|
|
13596
14150
|
message: "this client was generated without computed fields \u2014 declare them in vibeorm.config.ts and re-run vibeorm generate",
|
|
13597
14151
|
meta: {}
|
|
@@ -13616,15 +14170,16 @@ function createClient(params) {
|
|
|
13616
14170
|
listeners: new Set,
|
|
13617
14171
|
renderCache: new StatementRenderCache,
|
|
13618
14172
|
...telemetry === undefined ? {} : { telemetry },
|
|
14173
|
+
...diagnostics === undefined ? {} : { diagnostics },
|
|
13619
14174
|
...extensions?.mounts === undefined ? {} : { extensionMounts: extensions.mounts },
|
|
13620
14175
|
...connectHooks.length === 0 ? {} : { connectHooks }
|
|
13621
14176
|
}
|
|
13622
14177
|
});
|
|
13623
14178
|
}
|
|
13624
14179
|
// packages/runtime/src/keyset-iterator.ts
|
|
13625
|
-
import { VibeError as
|
|
14180
|
+
import { VibeError as VibeError29 } from "@vibeorm/schema";
|
|
13626
14181
|
function validation(params) {
|
|
13627
|
-
return new
|
|
14182
|
+
return new VibeError29({ code: "VIBE_VALIDATION", message: params.message, meta: params.meta });
|
|
13628
14183
|
}
|
|
13629
14184
|
function projectionFor(params) {
|
|
13630
14185
|
const { plan: plan2, select, omit } = params;
|
|
@@ -13701,7 +14256,7 @@ function iterateKeyset(options) {
|
|
|
13701
14256
|
});
|
|
13702
14257
|
if (advanced === false) {
|
|
13703
14258
|
stop();
|
|
13704
|
-
throw new
|
|
14259
|
+
throw new VibeError29({
|
|
13705
14260
|
code: "VIBE_DATA_INVALID",
|
|
13706
14261
|
message: `keyset traversal of "${options.model}" is not advancing: the engine returned a row that does not sort after the previous position. ` + "The ordering is not total, or a cursor value lost precision \u2014 a DateTime column finer than milliseconds asserted as millisecond-exact does this.",
|
|
13707
14262
|
meta: { model: options.model, order: plan2.signature, reason: "keyset-position-not-advancing" }
|
|
@@ -13774,7 +14329,7 @@ function iterateKeyset(options) {
|
|
|
13774
14329
|
return iterator;
|
|
13775
14330
|
}
|
|
13776
14331
|
// packages/runtime/src/telemetry/collector.ts
|
|
13777
|
-
import { VibeError as
|
|
14332
|
+
import { VibeError as VibeError30 } from "@vibeorm/schema";
|
|
13778
14333
|
function createTelemetryCollector(params) {
|
|
13779
14334
|
const throwOn = new Set(params?.throwOn ?? []);
|
|
13780
14335
|
const operations = [];
|
|
@@ -13784,7 +14339,7 @@ function createTelemetryCollector(params) {
|
|
|
13784
14339
|
let shutdowns = 0;
|
|
13785
14340
|
const hostile = (phase) => {
|
|
13786
14341
|
if (throwOn.has(phase)) {
|
|
13787
|
-
throw new
|
|
14342
|
+
throw new VibeError30({
|
|
13788
14343
|
code: "VIBE_INTERNAL",
|
|
13789
14344
|
message: `telemetry collector: deliberate ${phase} failure`,
|
|
13790
14345
|
meta: { reason: "telemetry-collector-deliberate-failure", phase }
|
|
@@ -13840,7 +14395,7 @@ function createTelemetryCollector(params) {
|
|
|
13840
14395
|
operationFor: (lookup) => {
|
|
13841
14396
|
const matches = operations.filter((event) => event.method === lookup.method && (lookup.model === undefined || event.model === lookup.model));
|
|
13842
14397
|
if (matches.length > 1) {
|
|
13843
|
-
throw new
|
|
14398
|
+
throw new VibeError30({
|
|
13844
14399
|
code: "VIBE_VALIDATION",
|
|
13845
14400
|
message: `telemetry collector: ${matches.length} operations match ${lookup.model ?? "*"}.${lookup.method}`,
|
|
13846
14401
|
meta: {
|
|
@@ -13860,10 +14415,12 @@ export {
|
|
|
13860
14415
|
updateWithNested,
|
|
13861
14416
|
toSqlExecutor,
|
|
13862
14417
|
toClientName,
|
|
14418
|
+
summarizePlan,
|
|
13863
14419
|
sqlProjection,
|
|
13864
14420
|
sql2 as sql,
|
|
13865
14421
|
rewriteScopedCall,
|
|
13866
14422
|
resolveTelemetryConfig,
|
|
14423
|
+
resolveStatementDiagnostics,
|
|
13867
14424
|
resolveScopedModels,
|
|
13868
14425
|
resolveResultShape,
|
|
13869
14426
|
resolveRelationSelection,
|
|
@@ -13874,12 +14431,14 @@ export {
|
|
|
13874
14431
|
queryFingerprint,
|
|
13875
14432
|
previewOperation,
|
|
13876
14433
|
parsePostgresPlan,
|
|
14434
|
+
parseDiagnosticsEnv,
|
|
13877
14435
|
parameterTypeTag,
|
|
13878
14436
|
normalizeSql,
|
|
13879
14437
|
lateralColumnAlias,
|
|
13880
14438
|
keysetRowValues,
|
|
13881
14439
|
keysetRowIsAfter,
|
|
13882
14440
|
iterateKeyset,
|
|
14441
|
+
isExecutableRead,
|
|
13883
14442
|
isDbNow,
|
|
13884
14443
|
inspectNativeRlsTransaction,
|
|
13885
14444
|
inspectNativeRls,
|
|
@@ -13893,9 +14452,11 @@ export {
|
|
|
13893
14452
|
generateNanoid,
|
|
13894
14453
|
generateDefaultValue,
|
|
13895
14454
|
generateCuid,
|
|
14455
|
+
formatStatementDiagnostic,
|
|
13896
14456
|
formatNativeRlsReport,
|
|
13897
14457
|
explainSql,
|
|
13898
14458
|
explainOperation,
|
|
14459
|
+
explainOnSession,
|
|
13899
14460
|
encodeValue,
|
|
13900
14461
|
encodeKeysetToken,
|
|
13901
14462
|
delegateMethodAccess,
|
|
@@ -13911,6 +14472,7 @@ export {
|
|
|
13911
14472
|
createView,
|
|
13912
14473
|
createTelemetryRecorder,
|
|
13913
14474
|
createTelemetryCollector,
|
|
14475
|
+
createStatementDiagnostics,
|
|
13914
14476
|
createSqlModel,
|
|
13915
14477
|
createScopeRewriter,
|
|
13916
14478
|
createClient,
|
|
@@ -13942,6 +14504,7 @@ export {
|
|
|
13942
14504
|
WRITE_DELEGATE_METHODS,
|
|
13943
14505
|
ViewResult,
|
|
13944
14506
|
TELEMETRY_DEFAULTS,
|
|
14507
|
+
STATEMENT_DIAGNOSTICS_DEFAULTS,
|
|
13945
14508
|
SQLITE_CODECS,
|
|
13946
14509
|
SKIP,
|
|
13947
14510
|
READ_DELEGATE_METHODS,
|
|
@@ -13958,4 +14521,4 @@ export {
|
|
|
13958
14521
|
CLIENT_MEMBER_ACCESS
|
|
13959
14522
|
};
|
|
13960
14523
|
|
|
13961
|
-
//# debugId=
|
|
14524
|
+
//# debugId=66C6E11BD1A77F7364756E2164756E21
|