pipework 0.290.0 → 0.292.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/REFERENCE.md +81 -1
  3. package/dist/auth/tenant/scoped-db.d.ts +9 -0
  4. package/dist/auth/tenant/scoped-db.d.ts.map +1 -1
  5. package/dist/auth/tenant/scoped-db.js +14 -0
  6. package/dist/auth/tenant/scoped-db.js.map +1 -1
  7. package/dist/data/db/function-definition.d.ts +46 -0
  8. package/dist/data/db/function-definition.d.ts.map +1 -0
  9. package/dist/data/db/function-definition.js +84 -0
  10. package/dist/data/db/function-definition.js.map +1 -0
  11. package/dist/data/db/function-source.d.ts +33 -2
  12. package/dist/data/db/function-source.d.ts.map +1 -1
  13. package/dist/data/db/function-source.js +47 -19
  14. package/dist/data/db/function-source.js.map +1 -1
  15. package/dist/data/db/namespace.d.ts +5 -3
  16. package/dist/data/db/namespace.d.ts.map +1 -1
  17. package/dist/data/db/namespace.js +5 -3
  18. package/dist/data/db/namespace.js.map +1 -1
  19. package/dist/data/db/rows.d.ts +1 -0
  20. package/dist/data/db/rows.d.ts.map +1 -1
  21. package/dist/data/db/rows.js +1 -1
  22. package/dist/data/db/rows.js.map +1 -1
  23. package/dist/data/migrate/diff.d.ts +7 -0
  24. package/dist/data/migrate/diff.d.ts.map +1 -1
  25. package/dist/data/migrate/diff.js +37 -0
  26. package/dist/data/migrate/diff.js.map +1 -1
  27. package/dist/data/migrate/history.d.ts +2 -0
  28. package/dist/data/migrate/history.d.ts.map +1 -1
  29. package/dist/data/migrate/history.js +8 -0
  30. package/dist/data/migrate/history.js.map +1 -1
  31. package/dist/data/migrate/snapshot.d.ts +3 -0
  32. package/dist/data/migrate/snapshot.d.ts.map +1 -1
  33. package/dist/data/migrate/snapshot.js +13 -0
  34. package/dist/data/migrate/snapshot.js.map +1 -1
  35. package/dist/data/migrate/sql-emit.d.ts.map +1 -1
  36. package/dist/data/migrate/sql-emit.js +30 -2
  37. package/dist/data/migrate/sql-emit.js.map +1 -1
  38. package/dist/data/write/audit/verb-insert.d.ts.map +1 -1
  39. package/dist/data/write/audit/verb-insert.js +4 -1
  40. package/dist/data/write/audit/verb-insert.js.map +1 -1
  41. package/dist/data/write/audit/verb-update.d.ts.map +1 -1
  42. package/dist/data/write/audit/verb-update.js +14 -3
  43. package/dist/data/write/audit/verb-update.js.map +1 -1
  44. package/dist/data/write/batch-insert.d.ts +35 -0
  45. package/dist/data/write/batch-insert.d.ts.map +1 -0
  46. package/dist/data/write/batch-insert.js +171 -0
  47. package/dist/data/write/batch-insert.js.map +1 -0
  48. package/dist/data/write/execute.d.ts +1 -1
  49. package/dist/data/write/execute.d.ts.map +1 -1
  50. package/dist/data/write/execute.js +89 -2
  51. package/dist/data/write/execute.js.map +1 -1
  52. package/dist/data/write/index.d.ts +2 -0
  53. package/dist/data/write/index.d.ts.map +1 -1
  54. package/dist/data/write/index.js +1 -0
  55. package/dist/data/write/index.js.map +1 -1
  56. package/dist/data/write/insert.d.ts +8 -0
  57. package/dist/data/write/insert.d.ts.map +1 -1
  58. package/dist/data/write/insert.js +14 -0
  59. package/dist/data/write/insert.js.map +1 -1
  60. package/dist/data/write/state.d.ts +10 -0
  61. package/dist/data/write/state.d.ts.map +1 -1
  62. package/dist/data/write/update-each.d.ts +28 -0
  63. package/dist/data/write/update-each.d.ts.map +1 -0
  64. package/dist/data/write/update-each.js +104 -0
  65. package/dist/data/write/update-each.js.map +1 -0
  66. package/dist/index.d.ts +2 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/test/function-reads.d.ts +97 -0
  70. package/dist/test/function-reads.d.ts.map +1 -0
  71. package/dist/test/function-reads.js +202 -0
  72. package/dist/test/function-reads.js.map +1 -0
  73. package/dist/test/index.d.ts +1 -0
  74. package/dist/test/index.d.ts.map +1 -1
  75. package/dist/test/index.js +1 -0
  76. package/dist/test/index.js.map +1 -1
  77. package/package.json +2 -2
  78. package/src/auth/tenant/scoped-db.ts +13 -0
  79. package/src/data/db/function-definition.ts +172 -0
  80. package/src/data/db/function-source.ts +86 -14
  81. package/src/data/db/namespace.ts +5 -3
  82. package/src/data/db/rows.ts +1 -1
  83. package/src/data/migrate/diff.ts +45 -0
  84. package/src/data/migrate/history.ts +9 -0
  85. package/src/data/migrate/snapshot.ts +17 -0
  86. package/src/data/migrate/sql-emit.ts +33 -3
  87. package/src/data/write/audit/verb-insert.ts +4 -1
  88. package/src/data/write/audit/verb-update.ts +13 -3
  89. package/src/data/write/batch-insert.ts +179 -0
  90. package/src/data/write/execute.ts +77 -2
  91. package/src/data/write/index.ts +2 -0
  92. package/src/data/write/insert.ts +15 -0
  93. package/src/data/write/state.ts +6 -0
  94. package/src/data/write/update-each.ts +120 -0
  95. package/src/index.ts +2 -1
  96. package/src/test/function-reads.ts +290 -0
  97. package/src/test/index.ts +1 -0
@@ -0,0 +1,202 @@
1
+ import { sql } from '../data/db/sql.js';
2
+ import { quoteIdentifier } from '../data/db/identifiers.js';
3
+ function walk(node, visit) {
4
+ visit(node);
5
+ for (const child of node.Plans ?? [])
6
+ walk(child, visit);
7
+ }
8
+ function qualified(ref) {
9
+ return ref.schema === 'public' ? ref.name : `${ref.schema}.${ref.name}`;
10
+ }
11
+ /** Rows touched and the shape facts of one EXPLAIN (ANALYZE, VERBOSE, FORMAT JSON) plan. */
12
+ export function measurePlan(root, ref, history, statistics) {
13
+ const scans = [];
14
+ let calledAsFunction = false;
15
+ let cteScan = false;
16
+ const callText = new RegExp(`(?:^|[^\\w])${ref.name}\\s*\\(`);
17
+ walk(root, node => {
18
+ if (node['Relation Name'] !== undefined) {
19
+ const loops = node['Actual Loops'] ?? 1;
20
+ const rows = (node['Actual Rows'] ?? 0) + (node['Rows Removed by Filter'] ?? 0) + (node['Rows Removed by Index Recheck'] ?? 0);
21
+ scans.push({
22
+ node: node['Node Type'] ?? 'Scan',
23
+ relation: `${node['Schema'] ?? 'public'}.${node['Relation Name']}`,
24
+ touched: rows * loops,
25
+ });
26
+ }
27
+ if (node['Node Type'] === 'CTE Scan')
28
+ cteScan = true;
29
+ if (node['Function Name'] === ref.name)
30
+ calledAsFunction = true;
31
+ for (const [key, value] of Object.entries(node)) {
32
+ if (key === 'Plans')
33
+ continue;
34
+ if ((key === 'Output' || key === 'Function Call' || key.endsWith('Filter') || key === 'Join Filter' || key === 'Index Cond') && callText.test(JSON.stringify(value)))
35
+ calledAsFunction = true;
36
+ }
37
+ });
38
+ scans.sort((a, b) => b.touched - a.touched);
39
+ return { history, statistics, touched: scans.reduce((n, s) => n + s.touched, 0), scans, calledAsFunction, cteScan };
40
+ }
41
+ /** Why Postgres would not inline the function, from its catalog row: the settings a plan cannot show. */
42
+ export function catalogProblems(name, c) {
43
+ if (c === undefined)
44
+ return [`the function ${name} does not exist in the database. Run the migration that creates it before checking it.`];
45
+ const problems = [];
46
+ if (c.config !== null && c.config.length > 0) {
47
+ problems.push(`${name} has a SET clause (${c.config.join(', ')}). Postgres never inlines a function with a SET clause, so each call is a black box the planner cannot push keys into. ` +
48
+ 'Remove it: a session setting such as jit belongs on the role (`ALTER ROLE app SET jit = off`).');
49
+ }
50
+ if (c.language !== 'sql') {
51
+ problems.push(`${name} is LANGUAGE ${c.language}. Postgres inlines only LANGUAGE sql functions. Write the read as one SELECT; a computation that is not one SELECT is a derivation, not a read.`);
52
+ }
53
+ if (c.volatility === 'v')
54
+ problems.push(`${name} is VOLATILE. Postgres does not inline a volatile function. Declare it STABLE or IMMUTABLE.`);
55
+ if (c.securityDefiner)
56
+ problems.push(`${name} is SECURITY DEFINER. Postgres does not inline a security definer function.`);
57
+ if (c.strict)
58
+ problems.push(`${name} is STRICT. Postgres does not inline a strict set-returning function.`);
59
+ return problems;
60
+ }
61
+ async function readCatalog(db, ref) {
62
+ const signature = `${quoteIdentifier(ref.schema, 'schema')}.${quoteIdentifier(ref.name, 'function')}(${ref.args.join(', ')})`;
63
+ const rows = await db.execute(sql `
64
+ select l.lanname as lang, p.provolatile::text as vol, p.proconfig as config, p.prosecdef as secdef, p.proisstrict as strict
65
+ from pg_proc p join pg_language l on l.oid = p.prolang
66
+ where p.oid = to_regprocedure(${signature})`);
67
+ const r = rows[0];
68
+ return r === undefined ? undefined : { language: r.lang, volatility: r.vol, config: r.config, securityDefiner: r.secdef, strict: r.strict };
69
+ }
70
+ async function explain(db, opts, history, statistics) {
71
+ const source = opts.fn(...opts.args);
72
+ return await db.transaction(async (tx) => {
73
+ // Parallel workers would split the loop counts the measurement multiplies by.
74
+ await tx.execute(sql `set local max_parallel_workers_per_gather = 0`);
75
+ // The call is the typed source (or expression) pipe.fn returns, so the plan is the one the caller's own query gets.
76
+ const query = opts.fn.ref.setReturning
77
+ ? tx.select().from(source)
78
+ : sql `select ${source} as value`;
79
+ const rows = await tx.execute(sql `explain (analyze, verbose, format json) ${query}`);
80
+ const raw = rows[0]?.['QUERY PLAN'];
81
+ const parsed = (typeof raw === 'string' ? JSON.parse(raw) : raw);
82
+ return measurePlan(parsed[0].Plan, opts.fn.ref, history, statistics);
83
+ });
84
+ }
85
+ async function setAutovacuum(db, tables, on) {
86
+ for (const t of tables) {
87
+ try {
88
+ await db.execute(sql.raw(`alter table ${qualifiedTable(t)} ${on ? 'reset (autovacuum_enabled)' : 'set (autovacuum_enabled = false)'}`));
89
+ }
90
+ catch (err) {
91
+ throw new Error(`[pipework] checkFunctionRead cannot change autovacuum on ${t}: ${err instanceof Error ? err.message : String(err)}\n\n` +
92
+ ' Fix: run the check as the role that owns the tables (or a superuser). Autovacuum must be off on them for the run, or a background ANALYZE could turn the stale and missing measurements into fresh ones.\n');
93
+ }
94
+ }
95
+ }
96
+ function qualifiedTable(table) {
97
+ const parts = table.split('.');
98
+ if (parts.length > 2)
99
+ throw new Error(`[pipework] checkFunctionRead: "${table}" is not a table name; use "schema.table" or "table".`);
100
+ return parts.map(p => quoteIdentifier(p, 'table')).join('.');
101
+ }
102
+ const where = (m) => m.scans.length === 0 ? '' : ` Largest scan: ${m.scans[0].node} on ${m.scans[0].relation} (${m.scans[0].touched} rows).`;
103
+ /**
104
+ * Plans one call of a pipework-defined read at two history sizes and under three states of the planner's
105
+ * statistics, and reports whether it is a key lookup: inlined into the calling query, touching a number of rows
106
+ * that does not grow with history, and no more of them under stale or missing statistics than after ANALYZE.
107
+ * Rows touched is counted from `EXPLAIN (ANALYZE)`: each relation scan's rows returned plus rows removed by
108
+ * filter or recheck, times its loops, summed. A read that touches no rows on the fixture is `not-checked`, never passed.
109
+ *
110
+ * The statistics states: after ANALYZE at the small size the large history is seeded without analyzing (stale), then
111
+ * analyzed (fresh), then the tables' pg_statistic rows are deleted (missing). Deleting them needs a superuser, as the
112
+ * test cluster's role is; autovacuum is switched off on the tables for the run and restored.
113
+ */
114
+ export async function checkFunctionRead(db, opts) {
115
+ const ref = opts.fn.ref;
116
+ const name = qualified(ref);
117
+ const [small, large] = opts.history ?? [200, 2000];
118
+ const relative = opts.tolerance?.relative ?? 0.1;
119
+ const absolute = opts.tolerance?.absolute ?? 5;
120
+ const analyze = async () => { for (const t of opts.tables)
121
+ await db.execute(sql.raw(`analyze ${qualifiedTable(t)}`)); };
122
+ const problems = catalogProblems(name, await readCatalog(db, ref));
123
+ const measurements = [];
124
+ const role = (await db.execute(sql `select current_user::text as role, rolsuper as super from pg_roles where rolname = current_user`))[0];
125
+ const notChecked = [];
126
+ if (role?.super !== true) {
127
+ notChecked.push(`Missing statistics were not checked: role "${role?.role ?? 'unknown'}" is not a superuser, so it cannot delete the tables' pg_statistic rows. ` +
128
+ 'Run the check as a superuser (the disposable test cluster\'s role; PIPEWORK_TEST_SUPERUSER_URL).');
129
+ }
130
+ await setAutovacuum(db, opts.tables, false);
131
+ try {
132
+ await opts.seed(db, small);
133
+ await analyze();
134
+ const freshSmall = await explain(db, opts, small, 'fresh');
135
+ await opts.seed(db, large);
136
+ const stale = await explain(db, opts, large, 'stale');
137
+ await analyze();
138
+ const freshLarge = await explain(db, opts, large, 'fresh');
139
+ measurements.push(freshSmall, stale, freshLarge);
140
+ if (notChecked.length === 0) {
141
+ for (const t of opts.tables)
142
+ await db.execute(sql `delete from pg_statistic where starelid = ${qualifiedTable(t)}::regclass`);
143
+ measurements.push(await explain(db, opts, large, 'missing'));
144
+ }
145
+ }
146
+ finally {
147
+ await setAutovacuum(db, opts.tables, true);
148
+ }
149
+ return judgeFunctionRead(name, problems, measurements, { relative, absolute }, notChecked);
150
+ }
151
+ /**
152
+ * The verdict on four measurements (fresh at the small history, then stale, fresh and missing at the large one)
153
+ * and the problems the catalog already named. Pure, so each verdict can be tested without a database.
154
+ */
155
+ export function judgeFunctionRead(name, catalog, measurements, tolerance, notChecked = []) {
156
+ const [freshSmall, stale, freshLarge, missing] = measurements;
157
+ const allowed = (baseline) => baseline * (1 + tolerance.relative) + tolerance.absolute;
158
+ const problems = [...catalog];
159
+ const small = freshSmall.history;
160
+ const large = freshLarge.history;
161
+ const done = (status) => ({
162
+ function: name,
163
+ status: status === 'passed' && notChecked.length > 0 ? 'not-checked' : status,
164
+ problems: [...problems, ...notChecked],
165
+ notChecked,
166
+ measurements,
167
+ });
168
+ if (measurements.some(m => m.calledAsFunction)) {
169
+ if (problems.length === 0) {
170
+ problems.push(`${name} was not inlined: the plan calls it as a function (a Function Scan), so the planner could not push the call's keys into its body. ` +
171
+ 'Postgres inlines a function only when it is LANGUAGE sql, one SELECT, STABLE or IMMUTABLE, without SET, SECURITY DEFINER or STRICT, and (for a set) the body is a plain query.');
172
+ }
173
+ return done('failed');
174
+ }
175
+ if (freshLarge.touched === 0) {
176
+ if (problems.length > 0)
177
+ return done('failed');
178
+ problems.push(`${name} was not exercised: the call touched no rows on the fixture at ${large} rows of history. Seed rows for the keys in \`args\` so the read has something to look up.`);
179
+ return done('not-checked');
180
+ }
181
+ if (freshLarge.touched > allowed(freshSmall.touched)) {
182
+ problems.push(`${name} touched ${freshSmall.touched} rows at ${small} rows of history and ${freshLarge.touched} at ${large}: its rows touched grow with history, so it scans history instead of looking keys up.${where(freshLarge)}` +
183
+ (freshLarge.cteScan ? ' The plan has a CTE Scan: a MATERIALIZED CTE (or one used twice) is computed in full before the call\'s keys apply; use it once without MATERIALIZED, or write it as a subquery.' : '') +
184
+ ' Give the read an index on its key columns and filter on them in the body.');
185
+ }
186
+ for (const m of [stale, missing]) {
187
+ if (m !== undefined && m.touched > allowed(freshLarge.touched)) {
188
+ problems.push(`${name} touched ${m.touched} rows with ${m.statistics} statistics against ${freshLarge.touched} after ANALYZE (at ${large} rows of history): its plan depends on the statistics being current.${where(m)} ` +
189
+ 'Make the plan the only reasonable one: an index on the key columns, and a filter the index serves.');
190
+ }
191
+ }
192
+ return done(problems.length === 0 ? 'passed' : 'failed');
193
+ }
194
+ /** `checkFunctionRead`, throwing a `[pipework]` error unless the read passed; `not-checked` throws too. */
195
+ export async function expectFunctionRead(db, opts) {
196
+ const report = await checkFunctionRead(db, opts);
197
+ if (report.status !== 'passed') {
198
+ throw new Error(`[pipework] function read ${report.function} ${report.status === 'failed' ? 'is not a key lookup' : 'was not checked'}:\n\n${report.problems.map(p => ` - ${p}`).join('\n')}\n`);
199
+ }
200
+ return report;
201
+ }
202
+ //# sourceMappingURL=function-reads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"function-reads.js","sourceRoot":"","sources":["../../src/test/function-reads.ts"],"names":[],"mappings":"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"}
@@ -10,4 +10,5 @@ export { pipework as vitestPlugin, type PipeworkVitestOptions } from './vitest-p
10
10
  export { resolveVitestConfig, resolveVitestModules } from './vitest-workspace.js';
11
11
  export { tap, type ManagedConnection, type ConnectOptions } from '../data/db/pool.js';
12
12
  export { crawlFixtureSite, type CrawlFixtureOptions, type CrawlFixtureRoute, type CrawlFixtureRequest, type CrawlFixtureSite } from './crawl-fixture.js';
13
+ export { checkFunctionRead, expectFunctionRead, judgeFunctionRead, measurePlan, catalogProblems, type FunctionReadOptions, type FunctionReadReport, type FunctionReadMeasurement, type StatisticsState } from './function-reads.js';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,eAAe,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAC/I,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAC1F,OAAO,EACL,YAAY,EACZ,aAAa,EACb,yBAAyB,EACzB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,qBAAqB,GAC3B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AACtF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AACtO,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACnF,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,GAAG,EAAE,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACrF,OAAO,EAAE,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,eAAe,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAC/I,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAC1F,OAAO,EACL,YAAY,EACZ,aAAa,EACb,yBAAyB,EACzB,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,qBAAqB,GAC3B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AACtF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AACtO,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACnF,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,GAAG,EAAE,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACrF,OAAO,EAAE,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACxJ,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAA"}
@@ -10,4 +10,5 @@ export { pipework as vitestPlugin } from './vitest-plugin.js';
10
10
  export { resolveVitestConfig, resolveVitestModules } from './vitest-workspace.js';
11
11
  export { tap } from '../data/db/pool.js';
12
12
  export { crawlFixtureSite } from './crawl-fixture.js';
13
+ export { checkFunctionRead, expectFunctionRead, judgeFunctionRead, measurePlan, catalogProblems } from './function-reads.js';
13
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,eAAe,EAAE,gBAAgB,EAAwC,MAAM,YAAY,CAAA;AAC/I,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAC1F,OAAO,EACL,YAAY,EACZ,aAAa,EACb,yBAAyB,GAI1B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAwB,MAAM,YAAY,CAAA;AACtF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAkD,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAA6B,qBAAqB,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAA;AACtO,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,cAAc,EAA2B,MAAM,cAAc,CAAA;AACnF,OAAO,EAAE,QAAQ,IAAI,YAAY,EAA8B,MAAM,oBAAoB,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,GAAG,EAA+C,MAAM,oBAAoB,CAAA;AACrF,OAAO,EAAE,gBAAgB,EAAqG,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,eAAe,EAAE,gBAAgB,EAAwC,MAAM,YAAY,CAAA;AAC/I,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAC1F,OAAO,EACL,YAAY,EACZ,aAAa,EACb,yBAAyB,GAI1B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAwB,MAAM,YAAY,CAAA;AACtF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAkD,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAA6B,qBAAqB,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAA;AACtO,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,cAAc,EAA2B,MAAM,cAAc,CAAA;AACnF,OAAO,EAAE,QAAQ,IAAI,YAAY,EAA8B,MAAM,oBAAoB,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,GAAG,EAA+C,MAAM,oBAAoB,CAAA;AACrF,OAAO,EAAE,gBAAgB,EAAqG,MAAM,oBAAoB,CAAA;AACxJ,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAAyG,MAAM,qBAAqB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipework",
3
- "version": "0.290.0",
3
+ "version": "0.292.0",
4
4
  "description": "TypeScript framework for multi-tenant SaaS applications. PostgreSQL-only.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -210,7 +210,7 @@
210
210
  "mutate": "stryker run",
211
211
  "rust:fmt": "cargo fmt --all --check",
212
212
  "rust:clippy": "cargo clippy --workspace --all-targets -- -D warnings && cargo clippy -p pipework-query --all-targets --no-default-features --features all-types,audit,hashable-value,serde,thread-safe,postgres-driver -- -D warnings",
213
- "rust:test": "cargo test --workspace && cargo test -p pipework-query --features all-types,hashable-value,serde",
213
+ "rust:test": "set -a; [ ! -f .env.test.local ] || . ./.env.test.local; set +a; cargo test --workspace && cargo test -p pipework-query --features all-types,hashable-value,serde",
214
214
  "rust:check": "pnpm rust:fmt && pnpm rust:clippy && pnpm rust:test"
215
215
  }
216
216
  }
@@ -34,6 +34,19 @@ export function unscopedExecutor(db: DB): { readonly db: DB; readonly tenant: st
34
34
  return scoped === undefined ? { db, tenant: null } : { db: scoped.raw, tenant: scoped.tenantId }
35
35
  }
36
36
 
37
+ /**
38
+ * For a batch insert that writes its own SQL: runs the connection's policy for an insert into `table` (a guard,
39
+ * `pipe.system()` or a door refuses a tenant table exactly as `insert(table)` does) and returns the tenant column
40
+ * and value the rows must carry, or null when the table is not tenant-scoped or the handle has no tenant.
41
+ */
42
+ export function insertTenant(db: DB, table: unknown): { readonly columnKey: string; readonly tenantId: string } | null {
43
+ ;(db as unknown as { insert: (t: unknown) => unknown }).insert(table)
44
+ const scoped = scopedHandles.get(db as object)
45
+ if (scoped === undefined) return null
46
+ const config = getScopeConfig(table as PgTable)
47
+ return config === null ? null : { columnKey: config.columnKey, tenantId: scoped.tenantId }
48
+ }
49
+
37
50
  /** The API's name and why it cannot be confined to one tenant, for the error a scoped handle gets. */
38
51
  function refuseScopedMessage(api: string, tenant: string, reason: string): string {
39
52
  return (
@@ -0,0 +1,172 @@
1
+ import { blankLiterals, stripComments } from '../migrate/statements.js'
2
+
3
+ /** Marks a callable returned by `pipe.fn` that carries a definition (a `body`), so the migration generator can find it among a schema file's exports. */
4
+ export const FUNCTION_DEFINITION = Symbol.for('pipework.fn.definition')
5
+
6
+ export type FunctionVolatility = 'stable' | 'immutable'
7
+
8
+ /** One column of a set-returning function's result, as the snapshot and the DDL carry it. */
9
+ export interface FunctionColumnDefinition {
10
+ readonly name: string
11
+ readonly type: string
12
+ }
13
+
14
+ /** A function pipework creates and migrates: what the snapshot records and the DDL is emitted from. */
15
+ export interface FunctionDefinition {
16
+ readonly schema: string
17
+ readonly name: string
18
+ /** SQL types of the arguments, in call order. */
19
+ readonly args: readonly string[]
20
+ /** `table`: RETURNS TABLE(columns) with a ROWS estimate. `scalar`: RETURNS type. */
21
+ readonly returns:
22
+ | { readonly kind: 'table'; readonly columns: readonly FunctionColumnDefinition[]; readonly rows: number }
23
+ | { readonly kind: 'scalar'; readonly type: string }
24
+ readonly body: string
25
+ readonly volatility: FunctionVolatility
26
+ }
27
+
28
+ /** What `pipe.fn` was given, for the rules that look at it before it becomes a definition. */
29
+ export interface FunctionDefinitionInput {
30
+ readonly name: string
31
+ readonly argCount: number
32
+ readonly body: string
33
+ readonly spec: Readonly<Record<string, unknown>>
34
+ readonly setReturning: boolean
35
+ }
36
+
37
+ const SPEC_KEYS = new Set(['args', 'columns', 'returns', 'body', 'volatility', 'rows', 'language'])
38
+
39
+ function fail(name: string, message: string, fix: string): never {
40
+ throw new Error(`[pipework] pipe.fn("${name}"): ${message}\n\n Fix: ${fix}\n`)
41
+ }
42
+
43
+ /** The identity of a function in a snapshot: `schema.name(type, type)`; a changed argument list is a different function. */
44
+ export function functionKey(def: Pick<FunctionDefinition, 'schema' | 'name' | 'args'>): string {
45
+ return `${def.schema}.${def.name}(${def.args.join(', ')})`
46
+ }
47
+
48
+ /**
49
+ * The static rules of a pipework-defined read. Each exists because Postgres inlines a SQL function into the
50
+ * calling query only when the function allows it, and a function that is not inlined is a black box the planner
51
+ * cannot push the caller's keys into; its rows touched then grow with history instead of with its keys.
52
+ * Throws a `[pipework]` error naming the rule and the fix.
53
+ */
54
+ export function assertReadFunction(input: FunctionDefinitionInput): void {
55
+ const { name, spec, body } = input
56
+
57
+ for (const key of ['set', 'config', 'settings']) {
58
+ if (key in spec) {
59
+ fail(
60
+ name,
61
+ `"${key}" is a SET clause, which a read function cannot have. Postgres never inlines a function with a SET clause (proconfig is set), so the planner cannot push the caller's keys into it and every call plans and scans on its own.`,
62
+ 'remove it. A session setting such as jit belongs on the role (`ALTER ROLE app SET jit = off`), not on a function.',
63
+ )
64
+ }
65
+ }
66
+ const unknown = Object.keys(spec).filter(k => !SPEC_KEYS.has(k) && k !== 'set' && k !== 'config' && k !== 'settings')
67
+ if (unknown.length > 0) {
68
+ fail(
69
+ name,
70
+ `unknown option${unknown.length > 1 ? 's' : ''} ${unknown.map(k => `"${k}"`).join(', ')}.`,
71
+ `a read function takes ${[...SPEC_KEYS].join(', ')}. SECURITY DEFINER, STRICT, PARALLEL and COST are not offered: each stops Postgres inlining the function.`,
72
+ )
73
+ }
74
+
75
+ const language = spec['language']
76
+ if (language !== undefined && language !== 'sql') {
77
+ fail(
78
+ name,
79
+ `language "${String(language)}" is not allowed for a read. Postgres inlines only LANGUAGE sql functions; a plpgsql function is a black box that runs per call and cannot be pushed into.`,
80
+ 'write the read as one LANGUAGE sql SELECT. A computation that cannot be one SELECT is a derivation (maintained on write), not a read.',
81
+ )
82
+ }
83
+
84
+ const volatility = spec['volatility']
85
+ if (volatility !== undefined && volatility !== 'stable' && volatility !== 'immutable') {
86
+ fail(
87
+ name,
88
+ `volatility "${String(volatility)}" is not allowed for a read. Postgres does not inline a VOLATILE function into the calling query.`,
89
+ "use 'stable' (reads tables) or 'immutable' (reads nothing that changes).",
90
+ )
91
+ }
92
+
93
+ if (input.setReturning) {
94
+ const rows = spec['rows']
95
+ if (typeof rows !== 'number' || !Number.isInteger(rows) || rows <= 0) {
96
+ fail(
97
+ name,
98
+ 'a set-returning function needs a ROWS estimate (a positive integer). Without it Postgres assumes 1000 rows for the call, and plans every join and filter around that guess.',
99
+ 'give `rows` the typical result size for one call, e.g. `rows: 50`.',
100
+ )
101
+ }
102
+ } else if (spec['rows'] !== undefined) {
103
+ fail(name, '`rows` applies to a set-returning function, and this one returns a scalar.', 'remove `rows`, or declare `columns` to return a set.')
104
+ }
105
+
106
+ const code = blankLiterals(stripComments(body))
107
+ const trimmed = code.trim().replace(/;\s*$/, '')
108
+
109
+ if (/;/.test(trimmed)) {
110
+ fail(
111
+ name,
112
+ 'the body has more than one statement. Postgres inlines a function only when its body is a single SELECT.',
113
+ 'make the body one SELECT (use a CTE or a join for the second step).',
114
+ )
115
+ }
116
+ if (!/^(select|with|\(\s*select)\b/i.test(trimmed)) {
117
+ fail(
118
+ name,
119
+ 'the body is not a SELECT. A read function is one SELECT statement; Postgres inlines only a single SELECT.',
120
+ 'start the body with SELECT or WITH ... SELECT.',
121
+ )
122
+ }
123
+ if (/\b(insert\s+into|update\s+[\w".]+\s+set|delete\s+from|merge\s+into|truncate\s+(?:table\s+)?["\w])/i.test(trimmed)) {
124
+ fail(
125
+ name,
126
+ 'the body writes (INSERT, UPDATE, DELETE, MERGE or TRUNCATE, possibly inside a CTE). A read function is one SELECT; a body with a data-modifying statement is not inlined.',
127
+ 'move the write to pipe.insert / pipe.update / pipe.delete.',
128
+ )
129
+ }
130
+ if (/\bfor\s+(no\s+key\s+update|update|key\s+share|share)\b/i.test(trimmed)) {
131
+ fail(name, 'the body takes row locks (FOR UPDATE / FOR SHARE). Postgres does not inline a function that locks rows.', 'remove the locking clause; a read does not lock.')
132
+ }
133
+ if (/\bset_config\s*\(/i.test(trimmed)) {
134
+ fail(
135
+ name,
136
+ 'the body calls set_config(), which is a SET clause in disguise: it makes the function VOLATILE and Postgres does not inline it.',
137
+ 'remove it. A session setting belongs on the role (`ALTER ROLE app SET ... `).',
138
+ )
139
+ }
140
+ if (/\bas\s+materialized\b/i.test(trimmed)) {
141
+ fail(
142
+ name,
143
+ 'the body has a MATERIALIZED CTE. Postgres computes it once, in full, before the caller\'s keys are applied: it is an optimisation fence, and the rows it scans grow with history, not with the keys.',
144
+ 'drop the MATERIALIZED keyword (a CTE used once is inlined by default), or write the CTE as a subquery or a join.',
145
+ )
146
+ }
147
+ if (/\bin\s*\(\s*\$\d+\s*(?:,\s*\$\d+\s*)+\)/i.test(trimmed) || /\barray\s*\[\s*\$\d+\s*(?:,\s*\$\d+\s*)+\]/i.test(trimmed)) {
148
+ fail(
149
+ name,
150
+ 'a list is passed as separate parameters ($1, $2, ...). A list argument is one array parameter: separate parameters give every list length its own signature and its own plan.',
151
+ 'declare one array argument (`pipe.field.uuid().array()`) and write `column = ANY($n)`.',
152
+ )
153
+ }
154
+ for (const m of trimmed.matchAll(/\$(\d+)/g)) {
155
+ const n = Number(m[1])
156
+ if (n < 1 || n > input.argCount) {
157
+ fail(
158
+ name,
159
+ `the body refers to $${n}, but the function declares ${input.argCount} argument${input.argCount === 1 ? '' : 's'}.`,
160
+ 'refer to arguments as $1 ... $n, in the order of `args`.',
161
+ )
162
+ }
163
+ }
164
+ }
165
+
166
+ /** The dollar-quote tag for a body: `$pw_body$`, lengthened until the body does not contain it. */
167
+ export function bodyQuote(body: string): string {
168
+ let tag = '$pw_body$'
169
+ let i = 0
170
+ while (body.includes(tag)) tag = `$pw_body${++i}$`
171
+ return tag
172
+ }
@@ -9,6 +9,7 @@ import type { InferFieldOutput } from '../domain/brand.js'
9
9
  import type { FieldDescriptor } from '../domain/types.js'
10
10
  import { projectTable } from '../domain/build-table.js'
11
11
  import { camelToSnake } from '../domain/naming.js'
12
+ import { assertReadFunction, FUNCTION_DEFINITION, type FunctionDefinition, type FunctionVolatility } from './function-definition.js'
12
13
 
13
14
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- variance-safe bound for descriptors with any output/flags
14
15
  type AnyField = FieldDescriptor<any, any>
@@ -21,6 +22,38 @@ export interface FunctionSpec<TArgs extends readonly AnyField[], TColumns extend
21
22
  readonly args: readonly [...TArgs]
22
23
  /** One `pipe.field.*` descriptor per result column; keys are camelCase, columns are their snake_case. */
23
24
  readonly columns: TColumns
25
+ /** The body of a function pipework creates: one `LANGUAGE sql` SELECT that refers to the arguments as `$1`, `$2`, ... Without it the function is only referenced (it exists already). */
26
+ readonly body?: string
27
+ /** `'stable'` (the default) or `'immutable'`; a read is never volatile. Only with `body`. */
28
+ readonly volatility?: FunctionVolatility
29
+ /** The typical number of rows one call returns, a planner estimate (ROWS). Required with `body`. */
30
+ readonly rows?: number
31
+ /** Only `'sql'`: Postgres inlines no other language. */
32
+ readonly language?: 'sql'
33
+ }
34
+
35
+ /** Declares a SQL function that returns one value. See {@link fn}. */
36
+ export interface ScalarFunctionSpec<TArgs extends readonly AnyField[], TReturn extends AnyField> {
37
+ readonly args: readonly [...TArgs]
38
+ readonly returns: TReturn
39
+ readonly body: string
40
+ readonly volatility?: FunctionVolatility
41
+ readonly language?: 'sql'
42
+ }
43
+
44
+ /** A function callable with typed arguments; `definition` is set when pipework creates it (the spec had a `body`). */
45
+ export type DefinedCall<TCall> = TCall & {
46
+ readonly definition: FunctionDefinition | undefined
47
+ /** Where the function lives and its argument types; what a plan or catalog check looks it up by. */
48
+ readonly ref: FunctionRef
49
+ }
50
+
51
+ /** A function's schema, name and SQL argument types. */
52
+ export interface FunctionRef {
53
+ readonly schema: string
54
+ readonly name: string
55
+ readonly args: readonly string[]
56
+ readonly setReturning: boolean
24
57
  }
25
58
 
26
59
  /** A column whose data type is `T` (and that cannot be null unless `T` can). */
@@ -56,7 +89,7 @@ function quote(name: string): string {
56
89
  }
57
90
 
58
91
  /**
59
- * Declares a set-returning SQL function and returns a callable that yields a source for `.from()` and joins.
92
+ * Declares a SQL function and returns a callable. A set-returning one (`columns`) yields a source for `.from()` and joins.
60
93
  * Each argument is a value, a column or a SQL expression of the declared type. A value is bound as a parameter,
61
94
  * converted the way a column of that field is on insert (jsonb as JSON text, dates, bigints, arrays), and cast to its
62
95
  * declared SQL type: `ext.f($1::uuid, $2::text)`. A null binds SQL NULL. An array argument (`field.uuid().array()`) is
@@ -64,10 +97,15 @@ function quote(name: string): string {
64
97
  * `unnest(t.arr::uuid[])`, so an array column can be unnested in a lateral join (`.crossJoinLateral(unnestUuids(t.arr))`).
65
98
  * Result columns are typed from `columns`; the default alias is the function's unqualified name.
66
99
  */
100
+ export function fn<const TArgs extends readonly AnyField[], TReturn extends AnyField>(
101
+ name: string,
102
+ spec: ScalarFunctionSpec<TArgs, TReturn>,
103
+ ): DefinedCall<(...values: ArgValues<TArgs>) => SQL<InferFieldOutput<TReturn>>>
67
104
  export function fn<const TArgs extends readonly AnyField[], TColumns extends Record<string, AnyField>>(
68
105
  name: string,
69
106
  spec: FunctionSpec<TArgs, TColumns>,
70
- ): (...values: ArgValues<TArgs>) => FunctionSource<string, TColumns> {
107
+ ): DefinedCall<(...values: ArgValues<TArgs>) => FunctionSource<string, TColumns>>
108
+ export function fn(name: string, spec: FunctionSpec<readonly AnyField[], Record<string, AnyField>> | ScalarFunctionSpec<readonly AnyField[], AnyField>): unknown {
71
109
  const parts = name.split('.')
72
110
  if (parts.length > 2 || parts.some(p => !NAME_PART.test(p))) {
73
111
  fail(
@@ -75,29 +113,58 @@ export function fn<const TArgs extends readonly AnyField[], TColumns extends Rec
75
113
  'use "fn" or "schema.fn", each part lowercase letters, digits and underscores, not starting with a digit.',
76
114
  )
77
115
  }
78
- const keys = Object.keys(spec.columns)
79
- if (keys.length === 0) fail(`pipe.fn("${name}"): columns is empty.`, 'declare at least one result column.')
116
+ const scalar = 'returns' in spec ? spec.returns : undefined
117
+ const columns: Record<string, AnyField> = 'columns' in spec ? spec.columns : {}
118
+ const keys = Object.keys(columns)
119
+ if (scalar === undefined && keys.length === 0) fail(`pipe.fn("${name}"): columns is empty.`, 'declare at least one result column.')
120
+ if (scalar !== undefined && 'columns' in spec) fail(`pipe.fn("${name}"): both returns and columns are declared.`, 'declare `returns` for a scalar function or `columns` for a set-returning one.')
80
121
  for (const key of keys) {
81
122
  if (key === 'as' || key === '_') {
82
123
  fail(`pipe.fn("${name}"): a result column cannot be named "${key}".`, 'rename the key; the column name comes from camelToSnake(key).')
83
124
  }
84
125
  }
85
126
 
127
+ const sqlTypeOf = (fields: Record<string, AnyField>): Record<string, string> => {
128
+ const cols = getTableColumns(projectTable('pw_fn_args', fields, {})) as Record<string, { getSQLType(): string }>
129
+ return Object.fromEntries(Object.entries(cols).map(([k, c]) => {
130
+ const t = c.getSQLType()
131
+ return [k, t === 'serial' ? 'integer' : t === 'bigserial' ? 'bigint' : t]
132
+ }))
133
+ }
134
+
86
135
  const argFields: Record<string, AnyField> = {}
87
136
  spec.args.forEach((a, i) => { argFields[`a${i}`] = a })
88
137
  const argCols = getTableColumns(projectTable('pw_fn_args', argFields, {})) as Record<string, { getSQLType(): string; mapToDriverValue(value: unknown): unknown }>
89
- const argTypes = spec.args.map((_, i) => {
90
- const t = argCols[`a${i}`]!.getSQLType()
91
- return t === 'serial' ? 'integer' : t === 'bigserial' ? 'bigint' : t
92
- })
138
+ const argTypes = spec.args.map((_, i) => sqlTypeOf({ a: spec.args[i]! })['a']!)
139
+
140
+ let definition: FunctionDefinition | undefined
141
+ if (spec.body !== undefined) {
142
+ if (typeof spec.body !== 'string') fail(`pipe.fn("${name}"): body must be a string.`, 'pass the SELECT as a string, with arguments as $1, $2, ...')
143
+ assertReadFunction({ name, argCount: spec.args.length, body: spec.body, spec: spec as unknown as Record<string, unknown>, setReturning: scalar === undefined })
144
+ const returnTypes = scalar === undefined ? sqlTypeOf(columns) : sqlTypeOf({ r: scalar })
145
+ definition = {
146
+ schema: parts.length === 2 ? parts[0]! : 'public',
147
+ name: parts[parts.length - 1]!,
148
+ args: argTypes,
149
+ returns: scalar === undefined
150
+ ? { kind: 'table', columns: keys.map(k => ({ name: camelToSnake(k), type: returnTypes[k]! })), rows: (spec as FunctionSpec<readonly AnyField[], Record<string, AnyField>>).rows! }
151
+ : { kind: 'scalar', type: returnTypes['r']! },
152
+ body: spec.body,
153
+ volatility: spec.volatility ?? 'stable',
154
+ }
155
+ } else {
156
+ for (const key of ['volatility', 'rows', 'language'] as const) {
157
+ if (key in spec) fail(`pipe.fn("${name}"): "${key}" is given without a body.`, 'add `body` to have pipework create the function, or remove the option (a referenced function keeps what it was created with).')
158
+ }
159
+ }
93
160
 
94
161
  const fnSql = sql.raw(parts.map(quote).join('.'))
95
162
  const defaultAlias = parts[parts.length - 1]!
163
+ const argList = (values: readonly unknown[]): SQL => sql.join(values.map((v, i) => sql`${bindIfParam(v, argCols[`a${i}`] as unknown as Column)}::${sql.raw(argTypes[i]!)}`), sql`, `)
96
164
 
97
- const build = (values: readonly unknown[], alias: string): FunctionSource<string, TColumns> => {
165
+ const build = (values: readonly unknown[], alias: string): FunctionSource<string, Record<string, AnyField>> => {
98
166
  const columnList = sql.join(keys.map(k => sql.raw(quote(camelToSnake(k)))), sql`, `)
99
- const argList = sql.join(values.map((v, i) => sql`${bindIfParam(v, argCols[`a${i}`] as unknown as Column)}::${sql.raw(argTypes[i]!)}`), sql`, `)
100
- const inner = sql`select ${columnList} from ${fnSql}(${argList})`
167
+ const inner = sql`select ${columnList} from ${fnSql}(${argList(values)})`
101
168
  const fields: Record<string, SQL> = {}
102
169
  for (const k of keys) {
103
170
  fields[k] = sql.raw(`${quote(alias)}.${quote(camelToSnake(k))}`)
@@ -109,16 +176,21 @@ export function fn<const TArgs extends readonly AnyField[], TColumns extends Rec
109
176
  if (prop === 'as') return (next: string) => build(values, next)
110
177
  return selection.get(target, prop)
111
178
  },
112
- }) as unknown as FunctionSource<string, TColumns>
179
+ }) as unknown as FunctionSource<string, Record<string, AnyField>>
113
180
  }
114
181
 
115
- return (...values: ArgValues<TArgs>) => {
182
+ const call = (...values: unknown[]): unknown => {
116
183
  if (values.length !== spec.args.length) {
117
184
  fail(
118
185
  `pipe.fn("${name}"): called with ${values.length} argument(s), declared ${spec.args.length}.`,
119
186
  'pass one value per declared argument.',
120
187
  )
121
188
  }
122
- return build(values, defaultAlias)
189
+ return scalar !== undefined ? sql`${fnSql}(${argList(values)})` : build(values, defaultAlias)
123
190
  }
191
+ return Object.defineProperties(call, {
192
+ definition: { value: definition, enumerable: true },
193
+ ref: { value: { schema: parts.length === 2 ? parts[0]! : 'public', name: defaultAlias, args: argTypes, setReturning: scalar === undefined }, enumerable: true },
194
+ [FUNCTION_DEFINITION]: { value: definition },
195
+ })
124
196
  }