@prisma-next/adapter-postgres 0.16.0-dev.24 → 0.16.0-dev.26
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/{adapter-CuTsfsz3.mjs → adapter-DlDJIHgB.mjs} +2 -2
- package/dist/{adapter-CuTsfsz3.mjs.map → adapter-DlDJIHgB.mjs.map} +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/{control-adapter-Xog0HsuP.mjs → control-adapter-DKbwTazN.mjs} +81 -29
- package/dist/control-adapter-DKbwTazN.mjs.map +1 -0
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +2 -2
- package/dist/{descriptor-meta-DpA4aG-x.mjs → descriptor-meta-pc7bA0_C.mjs} +2 -2
- package/dist/{descriptor-meta-DpA4aG-x.mjs.map → descriptor-meta-pc7bA0_C.mjs.map} +1 -1
- package/dist/runtime.mjs +2 -2
- package/package.json +23 -23
- package/src/core/control-adapter.ts +118 -59
- package/dist/control-adapter-Xog0HsuP.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/adapter-postgres",
|
|
3
|
-
"version": "0.16.0-dev.
|
|
3
|
+
"version": "0.16.0-dev.26",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@prisma-next/contract": "0.16.0-dev.
|
|
9
|
-
"@prisma-next/contract-authoring": "0.16.0-dev.
|
|
10
|
-
"@prisma-next/errors": "0.16.0-dev.
|
|
11
|
-
"@prisma-next/family-sql": "0.16.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.16.0-dev.
|
|
13
|
-
"@prisma-next/ids": "0.16.0-dev.
|
|
14
|
-
"@prisma-next/psl-parser": "0.16.0-dev.
|
|
15
|
-
"@prisma-next/sql-contract": "0.16.0-dev.
|
|
16
|
-
"@prisma-next/sql-contract-psl": "0.16.0-dev.
|
|
17
|
-
"@prisma-next/sql-contract-ts": "0.16.0-dev.
|
|
18
|
-
"@prisma-next/sql-operations": "0.16.0-dev.
|
|
19
|
-
"@prisma-next/sql-relational-core": "0.16.0-dev.
|
|
20
|
-
"@prisma-next/sql-runtime": "0.16.0-dev.
|
|
21
|
-
"@prisma-next/sql-schema-ir": "0.16.0-dev.
|
|
22
|
-
"@prisma-next/target-postgres": "0.16.0-dev.
|
|
23
|
-
"@prisma-next/utils": "0.16.0-dev.
|
|
8
|
+
"@prisma-next/contract": "0.16.0-dev.26",
|
|
9
|
+
"@prisma-next/contract-authoring": "0.16.0-dev.26",
|
|
10
|
+
"@prisma-next/errors": "0.16.0-dev.26",
|
|
11
|
+
"@prisma-next/family-sql": "0.16.0-dev.26",
|
|
12
|
+
"@prisma-next/framework-components": "0.16.0-dev.26",
|
|
13
|
+
"@prisma-next/ids": "0.16.0-dev.26",
|
|
14
|
+
"@prisma-next/psl-parser": "0.16.0-dev.26",
|
|
15
|
+
"@prisma-next/sql-contract": "0.16.0-dev.26",
|
|
16
|
+
"@prisma-next/sql-contract-psl": "0.16.0-dev.26",
|
|
17
|
+
"@prisma-next/sql-contract-ts": "0.16.0-dev.26",
|
|
18
|
+
"@prisma-next/sql-operations": "0.16.0-dev.26",
|
|
19
|
+
"@prisma-next/sql-relational-core": "0.16.0-dev.26",
|
|
20
|
+
"@prisma-next/sql-runtime": "0.16.0-dev.26",
|
|
21
|
+
"@prisma-next/sql-schema-ir": "0.16.0-dev.26",
|
|
22
|
+
"@prisma-next/target-postgres": "0.16.0-dev.26",
|
|
23
|
+
"@prisma-next/utils": "0.16.0-dev.26",
|
|
24
24
|
"arktype": "^2.2.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@prisma-next/cli": "0.16.0-dev.
|
|
28
|
-
"@prisma-next/driver-postgres": "0.16.0-dev.
|
|
29
|
-
"@prisma-next/migration-tools": "0.16.0-dev.
|
|
30
|
-
"@prisma-next/test-utils": "0.16.0-dev.
|
|
31
|
-
"@prisma-next/tsconfig": "0.16.0-dev.
|
|
32
|
-
"@prisma-next/tsdown": "0.16.0-dev.
|
|
27
|
+
"@prisma-next/cli": "0.16.0-dev.26",
|
|
28
|
+
"@prisma-next/driver-postgres": "0.16.0-dev.26",
|
|
29
|
+
"@prisma-next/migration-tools": "0.16.0-dev.26",
|
|
30
|
+
"@prisma-next/test-utils": "0.16.0-dev.26",
|
|
31
|
+
"@prisma-next/tsconfig": "0.16.0-dev.26",
|
|
32
|
+
"@prisma-next/tsdown": "0.16.0-dev.26",
|
|
33
33
|
"pathe": "^2.0.3",
|
|
34
34
|
"tsdown": "0.22.8",
|
|
35
35
|
"typescript": "5.9.3",
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
SqlExecuteRequest,
|
|
32
32
|
} from '@prisma-next/sql-relational-core/ast';
|
|
33
33
|
import { isDdlNode } from '@prisma-next/sql-relational-core/ast';
|
|
34
|
+
import { parseWireName } from '@prisma-next/sql-schema-ir/naming';
|
|
34
35
|
import type {
|
|
35
36
|
PrimaryKeyInput,
|
|
36
37
|
SqlCheckConstraintIRInput,
|
|
@@ -49,21 +50,24 @@ import type {
|
|
|
49
50
|
AddColumnAction,
|
|
50
51
|
AlterTableActionVisitor,
|
|
51
52
|
DropDefaultAction,
|
|
53
|
+
PostgresAlterIndexRename,
|
|
52
54
|
PostgresAlterPolicyRename,
|
|
53
55
|
PostgresAlterTable,
|
|
56
|
+
PostgresCreateIndex,
|
|
54
57
|
PostgresCreatePolicy,
|
|
55
58
|
PostgresCreateSchema,
|
|
56
59
|
PostgresCreateTable,
|
|
57
60
|
PostgresCreateType,
|
|
58
61
|
PostgresDdlNode,
|
|
59
62
|
PostgresDisableRowLevelSecurity,
|
|
63
|
+
PostgresDropIndex,
|
|
60
64
|
PostgresDropPolicy,
|
|
61
65
|
PostgresDropType,
|
|
62
66
|
RlsPolicyOperation,
|
|
63
67
|
} from '@prisma-next/target-postgres/ddl';
|
|
64
68
|
import { parsePostgresDefault } from '@prisma-next/target-postgres/default-normalizer';
|
|
69
|
+
import { postgresError } from '@prisma-next/target-postgres/errors';
|
|
65
70
|
import { normalizeSchemaNativeType } from '@prisma-next/target-postgres/native-type-normalizer';
|
|
66
|
-
import { parseRlsPolicyWireName } from '@prisma-next/target-postgres/rls-canonicalize';
|
|
67
71
|
import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
|
|
68
72
|
import {
|
|
69
73
|
PostgresDatabaseSchemaNode,
|
|
@@ -870,8 +874,9 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
870
874
|
tablename: string;
|
|
871
875
|
indexname: string;
|
|
872
876
|
indisunique: boolean;
|
|
873
|
-
|
|
877
|
+
where_predicate: string | null;
|
|
874
878
|
attname: string | null;
|
|
879
|
+
element_def: string | null;
|
|
875
880
|
index_position: number;
|
|
876
881
|
amname: string | null;
|
|
877
882
|
reloptions: string[] | null;
|
|
@@ -885,12 +890,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
885
890
|
// the table order — verification compares against the contract
|
|
886
891
|
// with order-sensitive equality and reports a spurious
|
|
887
892
|
// `index_mismatch`.
|
|
893
|
+
//
|
|
894
|
+
// `element_def` is the per-position element text as Postgres reprints
|
|
895
|
+
// it (`pg_get_indexdef` with pretty-printing); an expression element
|
|
896
|
+
// has attnum 0 so its `attname` is null and the element text is the
|
|
897
|
+
// only faithful capture. A mixed index (columns + expressions) carries
|
|
898
|
+
// the WHOLE element list as one opaque string, so the reprint must
|
|
899
|
+
// fire for every position of an expression-carrying index — the CASE
|
|
900
|
+
// bounds the per-element catalog reconstruction to those indexes,
|
|
901
|
+
// which keeps pure-column schemas free of the reprint cost.
|
|
902
|
+
// `where_predicate` is the reprinted partial-index predicate, null
|
|
903
|
+
// for total indexes.
|
|
888
904
|
`SELECT
|
|
889
905
|
i.tablename,
|
|
890
906
|
i.indexname,
|
|
891
907
|
ix.indisunique,
|
|
892
|
-
ix.indpred
|
|
908
|
+
pg_get_expr(ix.indpred, ix.indrelid) AS where_predicate,
|
|
893
909
|
a.attname,
|
|
910
|
+
CASE WHEN 0 = ANY(ix.indkey::int[]) THEN pg_get_indexdef(ix.indexrelid, k.ord::int, true) END AS element_def,
|
|
894
911
|
k.ord AS index_position,
|
|
895
912
|
am.amname,
|
|
896
913
|
ic.reloptions
|
|
@@ -1128,39 +1145,24 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1128
1145
|
dependsOn: postgresColumnDependsOn(schema, tableName, uq.columns),
|
|
1129
1146
|
}));
|
|
1130
1147
|
|
|
1131
|
-
// Process indexes
|
|
1148
|
+
// Process indexes — keyed by catalog-unique index name, so two same-
|
|
1149
|
+
// tuple siblings (a unique index beside a redundant plain index) and
|
|
1150
|
+
// expression indexes all enter the tree at full fidelity.
|
|
1132
1151
|
const indexesMap = new Map<
|
|
1133
1152
|
string,
|
|
1134
1153
|
{
|
|
1135
|
-
columns: string[];
|
|
1136
1154
|
name: string;
|
|
1155
|
+
elements: { attname: string | null; elementDef: string | null }[];
|
|
1137
1156
|
unique: boolean;
|
|
1138
|
-
|
|
1157
|
+
where: string | null;
|
|
1139
1158
|
type: string | undefined;
|
|
1140
1159
|
options: Record<string, string> | undefined;
|
|
1141
1160
|
}
|
|
1142
1161
|
>();
|
|
1143
|
-
// An index with an expression key (e.g. `lower(email)`) reports that
|
|
1144
|
-
// key's row with `attname = null` (Postgres attribute numbers are
|
|
1145
|
-
// <= 0 for expressions, which the LEFT JOIN above can't resolve to a
|
|
1146
|
-
// real column). Every row for that index name is skipped below
|
|
1147
|
-
// rather than only the expression row, so the index never enters
|
|
1148
|
-
// `indexesMap` with a collapsed, misleading column list — a
|
|
1149
|
-
// two-column expression index silently reduced to its one real
|
|
1150
|
-
// column can coincide with an unrelated real single-column index,
|
|
1151
|
-
// and the schema differ's diff-tree node id is derived from the
|
|
1152
|
-
// column tuple (`sql-index/index:<columns>`), so two indexes
|
|
1153
|
-
// colliding on that tuple abort the diff with "duplicate id among
|
|
1154
|
-
// siblings" instead of a normal drift report.
|
|
1155
|
-
const indexNamesWithExpressionKey = new Set<string>();
|
|
1156
1162
|
for (const idxRow of indexesByTable.get(tableName) ?? []) {
|
|
1157
|
-
if (!idxRow.attname) {
|
|
1158
|
-
indexNamesWithExpressionKey.add(idxRow.indexname);
|
|
1159
|
-
continue;
|
|
1160
|
-
}
|
|
1161
1163
|
const existing = indexesMap.get(idxRow.indexname);
|
|
1162
1164
|
if (existing) {
|
|
1163
|
-
existing.
|
|
1165
|
+
existing.elements.push({ attname: idxRow.attname, elementDef: idxRow.element_def });
|
|
1164
1166
|
} else {
|
|
1165
1167
|
// Drop btree (the Postgres default) so a contract index without an
|
|
1166
1168
|
// explicit type matches a default-method introspected index without
|
|
@@ -1168,52 +1170,46 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1168
1170
|
const indexType = idxRow.amname && idxRow.amname !== 'btree' ? idxRow.amname : undefined;
|
|
1169
1171
|
const indexOptions = parsePgReloptions(idxRow.reloptions, idxRow.indexname);
|
|
1170
1172
|
indexesMap.set(idxRow.indexname, {
|
|
1171
|
-
columns: [idxRow.attname],
|
|
1172
1173
|
name: idxRow.indexname,
|
|
1174
|
+
elements: [{ attname: idxRow.attname, elementDef: idxRow.element_def }],
|
|
1173
1175
|
unique: idxRow.indisunique,
|
|
1174
|
-
|
|
1176
|
+
where: idxRow.where_predicate,
|
|
1175
1177
|
type: indexType,
|
|
1176
1178
|
options: indexOptions,
|
|
1177
1179
|
});
|
|
1178
1180
|
}
|
|
1179
1181
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
// index is a strict superset of what a plain index on the same
|
|
1189
|
-
// columns would add), otherwise the first by name for determinism.
|
|
1190
|
-
const survivingIndexes = Array.from(indexesMap.values()).filter(
|
|
1191
|
-
(idx) => !indexNamesWithExpressionKey.has(idx.name),
|
|
1192
|
-
);
|
|
1193
|
-
const bestByColumnTuple = new Map<string, (typeof survivingIndexes)[number]>();
|
|
1194
|
-
for (const idx of survivingIndexes) {
|
|
1195
|
-
const tupleKey = idx.columns.join(',');
|
|
1196
|
-
const existing = bestByColumnTuple.get(tupleKey);
|
|
1197
|
-
if (
|
|
1198
|
-
!existing ||
|
|
1199
|
-
(idx.unique && !existing.unique) ||
|
|
1200
|
-
(idx.unique === existing.unique && idx.name < existing.name)
|
|
1201
|
-
) {
|
|
1202
|
-
bestByColumnTuple.set(tupleKey, idx);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
const indexes: readonly SqlIndexIRInput[] = Array.from(bestByColumnTuple.values()).map(
|
|
1206
|
-
(idx) => ({
|
|
1207
|
-
columns: Object.freeze([...idx.columns]),
|
|
1182
|
+
const indexes: readonly SqlIndexIRInput[] = Array.from(indexesMap.values()).map((idx) => {
|
|
1183
|
+
// An expression element has attnum 0, which the attribute LEFT JOIN
|
|
1184
|
+
// cannot resolve — its attname is null. Any such element makes the
|
|
1185
|
+
// whole index an expression node: the entire element list (real
|
|
1186
|
+
// columns included) is carried as one opaque reprinted string.
|
|
1187
|
+
const isExpression = idx.elements.some((el) => el.attname === null);
|
|
1188
|
+
const columnNames = idx.elements.flatMap((el) => (el.attname !== null ? [el.attname] : []));
|
|
1189
|
+
const base = {
|
|
1208
1190
|
name: idx.name,
|
|
1191
|
+
// Rename-pass grouping only, like policy introspection: undefined
|
|
1192
|
+
// when the live name does not follow the wire-name shape.
|
|
1193
|
+
prefix: parseWireName(idx.name)?.prefix,
|
|
1194
|
+
where: idx.where ?? undefined,
|
|
1209
1195
|
unique: idx.unique,
|
|
1210
|
-
partial: idx.
|
|
1196
|
+
partial: idx.where !== null,
|
|
1211
1197
|
type: idx.type,
|
|
1212
1198
|
options: idx.options,
|
|
1213
1199
|
annotations: undefined,
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1200
|
+
// Expression indexes stamp chains to every column of the table —
|
|
1201
|
+
// the opaque expression is never parsed, so the deterministic
|
|
1202
|
+
// over-approximation keeps drops ordered.
|
|
1203
|
+
dependsOn: postgresColumnDependsOn(
|
|
1204
|
+
schema,
|
|
1205
|
+
tableName,
|
|
1206
|
+
isExpression ? Object.keys(columns) : columnNames,
|
|
1207
|
+
),
|
|
1208
|
+
};
|
|
1209
|
+
return isExpression
|
|
1210
|
+
? { ...base, expression: idx.elements.map((el) => el.elementDef ?? '').join(', ') }
|
|
1211
|
+
: { ...base, columns: Object.freeze([...columnNames]) };
|
|
1212
|
+
});
|
|
1217
1213
|
|
|
1218
1214
|
// Process check constraints — parse each predicate into column + value set.
|
|
1219
1215
|
// Only the two shapes emitted by this slice are recognised; free-form
|
|
@@ -1283,7 +1279,7 @@ export class PostgresControlAdapter implements SqlControlAdapter<'postgres'> {
|
|
|
1283
1279
|
...new Set(parsePgNameArray(row.roles).map((r) => r.toLowerCase())),
|
|
1284
1280
|
].sort();
|
|
1285
1281
|
const permissive = row.permissive.toUpperCase() === 'PERMISSIVE';
|
|
1286
|
-
const prefix =
|
|
1282
|
+
const prefix = parseWireName(row.policyname)?.prefix ?? row.policyname;
|
|
1287
1283
|
const policy = new PostgresPolicySchemaNode({
|
|
1288
1284
|
name: row.policyname,
|
|
1289
1285
|
prefix,
|
|
@@ -2010,6 +2006,65 @@ function pgRenderAlterPolicyRename(node: PostgresAlterPolicyRename): SqlExecuteR
|
|
|
2010
2006
|
};
|
|
2011
2007
|
}
|
|
2012
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* Renders one index reloption value: strings single-quote-escaped, finite
|
|
2011
|
+
* numbers verbatim, booleans in the `on`/`off` catalog spelling (the
|
|
2012
|
+
* canonical form the wire hash and the option equality commit to).
|
|
2013
|
+
*/
|
|
2014
|
+
function pgRenderIndexOptionValue(key: string, value: unknown): string {
|
|
2015
|
+
if (typeof value === 'string') return `'${escapeLiteral(value)}'`;
|
|
2016
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
2017
|
+
if (typeof value === 'boolean') return value ? 'on' : 'off';
|
|
2018
|
+
throw postgresError(
|
|
2019
|
+
'CONTRACT.INDEX_INVALID',
|
|
2020
|
+
`Index option "${key}" must be a string, finite number, or boolean; got ${typeof value}`,
|
|
2021
|
+
{ meta: { key, valueType: typeof value } },
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/** Qualifies an object name; an absent schema renders it unqualified (unbound namespace). */
|
|
2026
|
+
function pgQualify(schema: string | undefined, name: string): string {
|
|
2027
|
+
return schema === undefined
|
|
2028
|
+
? quoteIdentifier(name)
|
|
2029
|
+
: `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function pgRenderCreateIndex(node: PostgresCreateIndex): SqlExecuteRequest {
|
|
2033
|
+
const elementList =
|
|
2034
|
+
'columns' in node.elements
|
|
2035
|
+
? node.elements.columns.map(quoteIdentifier).join(', ')
|
|
2036
|
+
: node.elements.expression;
|
|
2037
|
+
const unique = node.unique ? 'UNIQUE ' : '';
|
|
2038
|
+
const using = node.type !== undefined ? ` USING ${quoteIdentifier(node.type)}` : '';
|
|
2039
|
+
const withClause =
|
|
2040
|
+
node.options !== undefined && Object.keys(node.options).length > 0
|
|
2041
|
+
? ` WITH (${Object.entries(node.options)
|
|
2042
|
+
.map(
|
|
2043
|
+
([key, value]) => `${quoteIdentifier(key)} = ${pgRenderIndexOptionValue(key, value)}`,
|
|
2044
|
+
)
|
|
2045
|
+
.join(', ')})`
|
|
2046
|
+
: '';
|
|
2047
|
+
const whereClause = node.where !== undefined ? ` WHERE (${node.where})` : '';
|
|
2048
|
+
return {
|
|
2049
|
+
sql: `CREATE ${unique}INDEX ${quoteIdentifier(node.name)} ON ${pgQualify(node.schema, node.table)}${using} (${elementList})${withClause}${whereClause}`,
|
|
2050
|
+
params: [],
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function pgRenderDropIndex(node: PostgresDropIndex): SqlExecuteRequest {
|
|
2055
|
+
return {
|
|
2056
|
+
sql: `DROP INDEX ${pgQualify(node.schema, node.name)}`,
|
|
2057
|
+
params: [],
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function pgRenderAlterIndexRename(node: PostgresAlterIndexRename): SqlExecuteRequest {
|
|
2062
|
+
return {
|
|
2063
|
+
sql: `ALTER INDEX ${pgQualify(node.schema, node.from)} RENAME TO ${quoteIdentifier(node.to)}`,
|
|
2064
|
+
params: [],
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2013
2068
|
function pgRenderDisableRowLevelSecurity(node: PostgresDisableRowLevelSecurity): SqlExecuteRequest {
|
|
2014
2069
|
const tableRef = `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}`;
|
|
2015
2070
|
return {
|
|
@@ -2032,6 +2087,10 @@ async function pgRenderDdlExecuteRequest(
|
|
|
2032
2087
|
dropPolicy: (node: PostgresDropPolicy) => Promise.resolve(pgRenderDropPolicy(node)),
|
|
2033
2088
|
alterPolicyRename: (node: PostgresAlterPolicyRename) =>
|
|
2034
2089
|
Promise.resolve(pgRenderAlterPolicyRename(node)),
|
|
2090
|
+
createIndex: (node: PostgresCreateIndex) => Promise.resolve(pgRenderCreateIndex(node)),
|
|
2091
|
+
dropIndex: (node: PostgresDropIndex) => Promise.resolve(pgRenderDropIndex(node)),
|
|
2092
|
+
alterIndexRename: (node: PostgresAlterIndexRename) =>
|
|
2093
|
+
Promise.resolve(pgRenderAlterIndexRename(node)),
|
|
2035
2094
|
disableRowLevelSecurity: (node: PostgresDisableRowLevelSecurity) =>
|
|
2036
2095
|
Promise.resolve(pgRenderDisableRowLevelSecurity(node)),
|
|
2037
2096
|
};
|