deepline 0.3.227 → 0.3.229
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/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/contracts.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/sheet-column-storage.ts +252 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +29 -8
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +250 -37
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +29 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-projection-diagnostics.ts +43 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +380 -183
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +17 -28
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -0
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +16 -2
- package/dist/bundling-sources/shared_libs/plays/execution-pipeline.ts +72 -0
- package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +1 -3
- package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +9 -13
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +28 -31
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/{compiler-manifest-Dpd2hEet.d.mts → compiler-manifest-n8ZtPv2h.d.mts} +31 -21
- package/dist/{compiler-manifest-Dpd2hEet.d.ts → compiler-manifest-n8ZtPv2h.d.ts} +31 -21
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -1
- package/dist/index.mjs +6 -1
- package/dist/install-integrity.json +4 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
|
|
|
200
200
|
// getters keep their established compatibility behavior.
|
|
201
201
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
202
202
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
203
|
-
version: '0.3.
|
|
203
|
+
version: '0.3.229',
|
|
204
204
|
updateSummary:
|
|
205
205
|
'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
|
|
206
206
|
packageCapabilities: {
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** Source evidence is advisory; stored values determine lossless SQL encoding. */
|
|
2
|
+
export interface PlayDatasetColumnTypeEvidence {
|
|
3
|
+
postgresTypeId: number;
|
|
4
|
+
valueType: 'boolean' | 'number' | 'string' | 'json';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type PlayDatasetColumnTypes = Record<
|
|
8
|
+
string,
|
|
9
|
+
PlayDatasetColumnTypeEvidence
|
|
10
|
+
>;
|
|
11
|
+
|
|
1
12
|
export type PlaySheetColumnSource =
|
|
2
13
|
| 'input'
|
|
3
14
|
| 'datasetColumn'
|
|
@@ -17,6 +28,7 @@ export interface PlaySheetColumnContract {
|
|
|
17
28
|
stepId?: string;
|
|
18
29
|
toolId?: string;
|
|
19
30
|
isRowKey?: boolean;
|
|
31
|
+
typeEvidence?: PlayDatasetColumnTypeEvidence;
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
export interface PlaySheetContract {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { PhysicalSheetColumnProjection } from './sheet-contract';
|
|
2
|
+
|
|
3
|
+
type QueryClient = {
|
|
4
|
+
query(
|
|
5
|
+
sql: string,
|
|
6
|
+
params?: unknown[],
|
|
7
|
+
): Promise<{ rows: Record<string, unknown>[] }>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type SheetIdentity = { schema: string; table: string };
|
|
11
|
+
type StorageType = 'boolean' | 'double precision' | 'text' | 'jsonb';
|
|
12
|
+
type ColumnState = { name: string; type: string; marker: string | null };
|
|
13
|
+
export type RuntimeSheetColumnTypes = ReadonlyMap<string, string>;
|
|
14
|
+
|
|
15
|
+
const MARKER = 'deepline:runtime-sheet-column:';
|
|
16
|
+
const UNDECIDED = `${MARKER}undecided`;
|
|
17
|
+
const NATIVE = `${MARKER}native`;
|
|
18
|
+
const MIXED = `${MARKER}jsonb`;
|
|
19
|
+
|
|
20
|
+
const identifier = (value: string) => `"${value.replaceAll('"', '""')}"`;
|
|
21
|
+
const literal = (value: string) => `'${value.replaceAll("'", "''")}'`;
|
|
22
|
+
const relation = (sheet: SheetIdentity) =>
|
|
23
|
+
`${identifier(sheet.schema)}.${identifier(sheet.table)}`;
|
|
24
|
+
|
|
25
|
+
function combine(
|
|
26
|
+
a: StorageType | null,
|
|
27
|
+
b: StorageType | null,
|
|
28
|
+
): StorageType | null {
|
|
29
|
+
return a === null ? b : b === null || a === b ? a : 'jsonb';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function valueType(value: unknown): StorageType | null {
|
|
33
|
+
if (value == null) return null;
|
|
34
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
35
|
+
if (typeof value === 'string') return 'text';
|
|
36
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
37
|
+
return 'double precision';
|
|
38
|
+
return 'jsonb';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function lock(client: QueryClient, sheet: SheetIdentity): Promise<void> {
|
|
42
|
+
// The caller holds this transaction through DML. Both SQL writers use the
|
|
43
|
+
// same lock, so type inspection, widening and the affected write are atomic.
|
|
44
|
+
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [
|
|
45
|
+
`runtime-sheet-columns:${relation(sheet)}`,
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function inspect(
|
|
50
|
+
client: QueryClient,
|
|
51
|
+
sheet: SheetIdentity,
|
|
52
|
+
): Promise<ColumnState[]> {
|
|
53
|
+
const result = await client.query(
|
|
54
|
+
`SELECT a.attname AS name, format_type(a.atttypid, NULL) AS type,
|
|
55
|
+
col_description(a.attrelid, a.attnum) AS marker
|
|
56
|
+
FROM pg_attribute a
|
|
57
|
+
WHERE a.attrelid = to_regclass($1)
|
|
58
|
+
AND a.attnum > 0 AND NOT a.attisdropped`,
|
|
59
|
+
[relation(sheet)],
|
|
60
|
+
);
|
|
61
|
+
return result.rows.map((row) => ({
|
|
62
|
+
name: String(row.name),
|
|
63
|
+
type: String(row.type),
|
|
64
|
+
marker: typeof row.marker === 'string' ? row.marker : null,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function mark(
|
|
69
|
+
client: QueryClient,
|
|
70
|
+
sheet: SheetIdentity,
|
|
71
|
+
column: string,
|
|
72
|
+
marker: string,
|
|
73
|
+
) {
|
|
74
|
+
await client.query(
|
|
75
|
+
`COMMENT ON COLUMN ${relation(sheet)}.${identifier(column)} IS ${literal(marker)}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function changeType(
|
|
80
|
+
client: QueryClient,
|
|
81
|
+
sheet: SheetIdentity,
|
|
82
|
+
column: ColumnState,
|
|
83
|
+
desired: StorageType | null,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
if (desired === null || column.marker === MIXED) return;
|
|
86
|
+
// Unmarked columns predate this feature (or belong to customers). Never
|
|
87
|
+
// reinterpret them merely because their current SQL type happens to match.
|
|
88
|
+
if (column.marker !== UNDECIDED && column.marker !== NATIVE) return;
|
|
89
|
+
let next =
|
|
90
|
+
column.marker === UNDECIDED
|
|
91
|
+
? desired
|
|
92
|
+
: combine(column.type as StorageType, desired)!;
|
|
93
|
+
if (column.marker === NATIVE && next === column.type) return;
|
|
94
|
+
const quoted = identifier(column.name);
|
|
95
|
+
|
|
96
|
+
if (column.marker === UNDECIDED && next !== 'jsonb') {
|
|
97
|
+
// Another compatible old writer may have filled a provisioned JSONB
|
|
98
|
+
// column before its first inference. Prove the whole stored column rather
|
|
99
|
+
// than relying on a preview or assuming the new marker means no data.
|
|
100
|
+
const jsonType =
|
|
101
|
+
next === 'text' ? 'string' : next === 'boolean' ? 'boolean' : 'number';
|
|
102
|
+
const incompatible = await client.query(
|
|
103
|
+
`SELECT EXISTS(SELECT 1 FROM ${relation(sheet)}
|
|
104
|
+
WHERE ${quoted} IS NOT NULL AND ${quoted} <> 'null'::jsonb
|
|
105
|
+
AND jsonb_typeof(${quoted}) <> $1) AS incompatible`,
|
|
106
|
+
[jsonType],
|
|
107
|
+
);
|
|
108
|
+
if (incompatible.rows[0]?.incompatible === true) next = 'jsonb';
|
|
109
|
+
if (next === 'double precision') {
|
|
110
|
+
// Existing arbitrary JSON numeric values may exceed IEEE-754 precision.
|
|
111
|
+
// CASE guards the cast; SQL predicate evaluation order is not a guard.
|
|
112
|
+
const numeric = `(${quoted} #>> '{}')::numeric`;
|
|
113
|
+
const loss = await client.query(
|
|
114
|
+
`SELECT EXISTS(SELECT 1 FROM ${relation(sheet)}
|
|
115
|
+
WHERE ${quoted} IS NOT NULL AND ${quoted} <> 'null'::jsonb
|
|
116
|
+
AND CASE WHEN abs(${numeric}) > 1.7976931348623157e308::numeric
|
|
117
|
+
OR (${numeric} <> 0 AND abs(${numeric}) < 5e-324::numeric)
|
|
118
|
+
THEN true
|
|
119
|
+
ELSE to_jsonb((${quoted} #>> '{}')::double precision) <> ${quoted}
|
|
120
|
+
END) AS incompatible`,
|
|
121
|
+
);
|
|
122
|
+
if (loss.rows[0]?.incompatible === true) next = 'jsonb';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (next !== column.type) {
|
|
127
|
+
const conversion =
|
|
128
|
+
next === 'jsonb'
|
|
129
|
+
? `to_jsonb(${quoted})`
|
|
130
|
+
: `(${quoted} #>> '{}')::${next}`;
|
|
131
|
+
await client.query(
|
|
132
|
+
`ALTER TABLE ${relation(sheet)} ALTER COLUMN ${quoted} TYPE ${next} USING ${conversion}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
await mark(client, sheet, column.name, next === 'jsonb' ? MIXED : NATIVE);
|
|
136
|
+
column.type = next;
|
|
137
|
+
column.marker = next === 'jsonb' ? MIXED : NATIVE;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Runtime Sheet column storage owns conservative, monotonic SQL typing.
|
|
142
|
+
* Methods that mutate storage require an existing transaction, held through
|
|
143
|
+
* the corresponding row write. JSON values remain the transport contract.
|
|
144
|
+
*/
|
|
145
|
+
export const RuntimeSheetColumns = {
|
|
146
|
+
lockForWrite: lock,
|
|
147
|
+
async markNewColumns(
|
|
148
|
+
client: QueryClient,
|
|
149
|
+
input: SheetIdentity & { columns: readonly string[] },
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
// Caller proves these columns were created in its schema transaction.
|
|
152
|
+
for (const column of input.columns)
|
|
153
|
+
await mark(client, input, column, UNDECIDED);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
async prepareWrite(
|
|
157
|
+
client: QueryClient,
|
|
158
|
+
input: SheetIdentity & {
|
|
159
|
+
rows:
|
|
160
|
+
| readonly Record<string, unknown>[]
|
|
161
|
+
| (() => Promise<readonly Record<string, unknown>[]>);
|
|
162
|
+
columns: readonly PhysicalSheetColumnProjection[];
|
|
163
|
+
},
|
|
164
|
+
): Promise<RuntimeSheetColumnTypes> {
|
|
165
|
+
await lock(client, input);
|
|
166
|
+
const rows =
|
|
167
|
+
typeof input.rows === 'function' ? await input.rows() : input.rows;
|
|
168
|
+
const columns = new Map(
|
|
169
|
+
(await inspect(client, input)).map((column) => [column.name, column]),
|
|
170
|
+
);
|
|
171
|
+
for (const projection of rows.length > 0 ? input.columns : []) {
|
|
172
|
+
const column = columns.get(projection.sqlName);
|
|
173
|
+
if (!column)
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Runtime Sheet column is missing: ${projection.sqlName}`,
|
|
176
|
+
);
|
|
177
|
+
let desired: StorageType | null =
|
|
178
|
+
projection.typeEvidence?.valueType === 'json' ? 'jsonb' : null;
|
|
179
|
+
for (const row of rows)
|
|
180
|
+
desired = combine(desired, valueType(row[projection.fieldName]));
|
|
181
|
+
await changeType(client, input, column, desired);
|
|
182
|
+
}
|
|
183
|
+
return new Map([...columns].map(([name, column]) => [name, column.type]));
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async prepareClone(
|
|
187
|
+
client: QueryClient,
|
|
188
|
+
input: SheetIdentity & {
|
|
189
|
+
sourceTable: string;
|
|
190
|
+
columns: readonly string[];
|
|
191
|
+
columnMappings?: readonly {
|
|
192
|
+
sourceSqlName: string;
|
|
193
|
+
targetSqlName: string;
|
|
194
|
+
}[];
|
|
195
|
+
},
|
|
196
|
+
): Promise<RuntimeSheetColumnTypes> {
|
|
197
|
+
const source = { schema: input.schema, table: input.sourceTable };
|
|
198
|
+
const sheets = [input, source].sort((a, b) =>
|
|
199
|
+
relation(a) < relation(b) ? -1 : relation(a) > relation(b) ? 1 : 0,
|
|
200
|
+
);
|
|
201
|
+
for (const sheet of sheets) await lock(client, sheet);
|
|
202
|
+
const sourceColumns = new Map(
|
|
203
|
+
(await inspect(client, source)).map((column) => [column.name, column]),
|
|
204
|
+
);
|
|
205
|
+
const targetColumns = new Map(
|
|
206
|
+
(await inspect(client, input)).map((column) => [column.name, column]),
|
|
207
|
+
);
|
|
208
|
+
const mappings =
|
|
209
|
+
input.columnMappings ??
|
|
210
|
+
input.columns.map((name) => ({
|
|
211
|
+
sourceSqlName: name,
|
|
212
|
+
targetSqlName: name,
|
|
213
|
+
}));
|
|
214
|
+
for (const mapping of mappings) {
|
|
215
|
+
const sourceColumn = sourceColumns.get(mapping.sourceSqlName);
|
|
216
|
+
const targetColumn = targetColumns.get(mapping.targetSqlName);
|
|
217
|
+
if (!sourceColumn || !targetColumn)
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Runtime Sheet clone column is missing: ${mapping.sourceSqlName} -> ${mapping.targetSqlName}`,
|
|
220
|
+
);
|
|
221
|
+
const type = sourceColumn.type;
|
|
222
|
+
await changeType(
|
|
223
|
+
client,
|
|
224
|
+
input,
|
|
225
|
+
targetColumn,
|
|
226
|
+
type === 'boolean' || type === 'text' || type === 'double precision'
|
|
227
|
+
? type
|
|
228
|
+
: 'jsonb',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return new Map(
|
|
232
|
+
[...targetColumns].map(([name, column]) => [name, column.type]),
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
jsonValueSql(
|
|
237
|
+
jsonExpression: string,
|
|
238
|
+
tableReference: string,
|
|
239
|
+
columnName: string,
|
|
240
|
+
types: RuntimeSheetColumnTypes,
|
|
241
|
+
): string {
|
|
242
|
+
const type = types.get(columnName);
|
|
243
|
+
if (!type)
|
|
244
|
+
throw new Error(`Runtime Sheet column is missing: ${columnName}`);
|
|
245
|
+
// jsonb_populate_record maps JSON null to SQL NULL. A legacy JSONB cell
|
|
246
|
+
// must retain explicit JSON null, distinct from an omitted/SQL NULL cell.
|
|
247
|
+
if (type === 'jsonb') return jsonExpression;
|
|
248
|
+
// Resolve the actual row type inside PostgreSQL, never from a cached
|
|
249
|
+
// contract. This also preserves JSON scalar strings for legacy JSONB.
|
|
250
|
+
return `(jsonb_populate_record(NULL::${tableReference}, jsonb_build_object(${literal(columnName)}, ${jsonExpression}))).${identifier(columnName)}`;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
PlayDatasetColumnTypeEvidence,
|
|
3
|
+
PlayDatasetColumnTypes,
|
|
4
|
+
PlaySheetContract,
|
|
5
|
+
} from './contracts';
|
|
2
6
|
import {
|
|
3
7
|
legacyPostgresTruncatedPlayColumnName,
|
|
4
8
|
sqlSafePlayColumnName,
|
|
@@ -7,6 +11,7 @@ import {
|
|
|
7
11
|
export type PhysicalSheetColumnProjection = {
|
|
8
12
|
sqlName: string;
|
|
9
13
|
fieldName: string;
|
|
14
|
+
typeEvidence?: PlayDatasetColumnTypeEvidence;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
export type LegacyPhysicalSheetColumnBackfill = {
|
|
@@ -37,13 +42,11 @@ export function isDatasetPayloadField(field: string): boolean {
|
|
|
37
42
|
|
|
38
43
|
export function physicalSheetColumnProjections(
|
|
39
44
|
sheetContract: PlaySheetContract | null | undefined,
|
|
45
|
+
physicalColumns?: readonly string[],
|
|
40
46
|
): PhysicalSheetColumnProjection[] {
|
|
41
|
-
if (!sheetContract) {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
47
|
const seen = new Set<string>();
|
|
45
48
|
const columns: PhysicalSheetColumnProjection[] = [];
|
|
46
|
-
for (const column of sheetContract
|
|
49
|
+
for (const column of sheetContract?.columns ?? []) {
|
|
47
50
|
const sqlName = column.sqlName.trim();
|
|
48
51
|
const fieldName =
|
|
49
52
|
column.source === 'input' || column.source === 'datasetColumn'
|
|
@@ -53,7 +56,18 @@ export function physicalSheetColumnProjections(
|
|
|
53
56
|
continue;
|
|
54
57
|
}
|
|
55
58
|
seen.add(sqlName);
|
|
56
|
-
columns.push({
|
|
59
|
+
columns.push({
|
|
60
|
+
sqlName,
|
|
61
|
+
fieldName,
|
|
62
|
+
...(column.typeEvidence ? { typeEvidence: column.typeEvidence } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// A prior run may have introduced fields absent from the current artifact.
|
|
66
|
+
// Keep their live storage projection so reads and writes do not hide them.
|
|
67
|
+
for (const sqlName of physicalColumns ?? []) {
|
|
68
|
+
if (!sqlName || sqlName.startsWith('_') || seen.has(sqlName)) continue;
|
|
69
|
+
seen.add(sqlName);
|
|
70
|
+
columns.push({ sqlName, fieldName: sqlName });
|
|
57
71
|
}
|
|
58
72
|
return columns;
|
|
59
73
|
}
|
|
@@ -141,7 +155,11 @@ export function outputPhysicalSheetColumnProjections(
|
|
|
141
155
|
continue;
|
|
142
156
|
}
|
|
143
157
|
seen.add(sqlName);
|
|
144
|
-
columns.push({
|
|
158
|
+
columns.push({
|
|
159
|
+
sqlName,
|
|
160
|
+
fieldName: column.field,
|
|
161
|
+
...(column.typeEvidence ? { typeEvidence: column.typeEvidence } : {}),
|
|
162
|
+
});
|
|
145
163
|
}
|
|
146
164
|
return columns;
|
|
147
165
|
}
|
|
@@ -158,6 +176,7 @@ export function augmentSheetContractWithDatasetFields(input: {
|
|
|
158
176
|
contract: PlaySheetContract;
|
|
159
177
|
rows: readonly Record<string, unknown>[];
|
|
160
178
|
outputFields?: readonly string[];
|
|
179
|
+
columnTypes?: PlayDatasetColumnTypes;
|
|
161
180
|
}): PlaySheetContract {
|
|
162
181
|
const outputFields = new Set(input.outputFields ?? []);
|
|
163
182
|
const candidateFields = new Set<string>();
|
|
@@ -198,7 +217,9 @@ export function augmentSheetContractWithDatasetFields(input: {
|
|
|
198
217
|
existingDatasetPayloadFields.add(field);
|
|
199
218
|
}
|
|
200
219
|
existingSqlNames.add(sqlName);
|
|
201
|
-
|
|
220
|
+
const typeEvidence =
|
|
221
|
+
column.source === 'input' ? input.columnTypes?.[field] : undefined;
|
|
222
|
+
target.push(typeEvidence ? { ...column, typeEvidence } : column);
|
|
202
223
|
};
|
|
203
224
|
|
|
204
225
|
for (const column of input.contract.columns) {
|