@platforma-sdk/workflow-tengo 4.5.2 → 4.6.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/index.d.ts +1 -1
- package/dist/index.js +6 -6
- package/dist/index.mjs +6 -6
- package/dist/tengo/lib/pframes.bundle.lib.tengo +37 -300
- package/dist/tengo/lib/pframes.lib.tengo +4 -1
- package/dist/tengo/lib/pframes.spec.lib.tengo +21 -1
- package/dist/tengo/lib/pframes.xsv-builder.lib.tengo +625 -0
- package/dist/tengo/lib/pt.expression.lib.tengo +905 -0
- package/dist/tengo/lib/pt.lib.tengo +980 -0
- package/dist/tengo/lib/tpl.base.lib.tengo +6 -1
- package/dist/tengo/tpl/check_network.create_workdir_from_storage.plj.gz +0 -0
- package/dist/tengo/tpl/check_network.download_blob.plj.gz +0 -0
- package/dist/tengo/tpl/check_network.run_hello_world.plj.gz +0 -0
- package/dist/tengo/tpl/check_network.run_hello_world_py.plj.gz +0 -0
- package/dist/tengo/tpl/check_network.upload_blob.plj.gz +0 -0
- package/dist/tengo/tpl/check_network.upload_template.plj.gz +0 -0
- package/dist/tengo/tpl/exec.R.create-renv.plj.gz +0 -0
- package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
- package/dist/tengo/tpl/exec.monetization_info.plj.gz +0 -0
- package/dist/tengo/tpl/exec.monetization_to_json.plj.gz +0 -0
- package/dist/tengo/tpl/exec.python.create-venv.plj.gz +0 -0
- package/dist/tengo/tpl/exec.python.install-deps.plj.gz +0 -0
- package/dist/tengo/tpl/exec.python.venv-bin-path.plj.gz +0 -0
- package/dist/tengo/tpl/ll.get-field.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-pframe-for-ui.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.process-pcolumn-data.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.remove-nulls.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.slice-data.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.xsv-export-pframe.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.xsv-import-if-not-null.plj.gz +0 -0
- package/dist/tengo/tpl/pframes.xsv-import-map.plj.gz +0 -0
- package/dist/tengo/tpl/waiter.waiter.plj.gz +0 -0
- package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
- package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
- package/dist/tengo/tpl/workflow.query-anchored.plj.gz +0 -0
- package/package.json +4 -3
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
ll := import("@platforma-sdk/workflow-tengo:ll")
|
|
2
|
+
exp := import("@platforma-sdk/workflow-tengo:pt.expression")
|
|
3
|
+
assets := import("@platforma-sdk/workflow-tengo:assets")
|
|
4
|
+
smart := import("@platforma-sdk/workflow-tengo:smart")
|
|
5
|
+
exec := import("@platforma-sdk/workflow-tengo:exec")
|
|
6
|
+
json := import("json")
|
|
7
|
+
text := import("text")
|
|
8
|
+
slices := import("@platforma-sdk/workflow-tengo:slices")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_newDataFrame := undefined
|
|
47
|
+
_newDataFrameGroupBy := undefined
|
|
48
|
+
|
|
49
|
+
workflowCounter := 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
workflow := func() {
|
|
58
|
+
steps := []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
inFiles := []
|
|
62
|
+
|
|
63
|
+
outFiles := []
|
|
64
|
+
|
|
65
|
+
outContentFiles := []
|
|
66
|
+
|
|
67
|
+
anonymousFilesCounter := 0
|
|
68
|
+
anonymousDFCounter := 0
|
|
69
|
+
|
|
70
|
+
self := undefined
|
|
71
|
+
|
|
72
|
+
id := workflowCounter
|
|
73
|
+
workflowCounter += 1
|
|
74
|
+
|
|
75
|
+
self = {
|
|
76
|
+
_wfId: id,
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
addRawStep: func(step) {
|
|
85
|
+
steps = append(steps, step)
|
|
86
|
+
return self
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
frame: func(frameInput, ...optionsRaw) {
|
|
127
|
+
opts := {}
|
|
128
|
+
if len(optionsRaw) > 0 {
|
|
129
|
+
if len(optionsRaw) == 1 && is_map(optionsRaw[0]) {
|
|
130
|
+
opts = optionsRaw[0]
|
|
131
|
+
} else {
|
|
132
|
+
ll.panic("frame options must be a single map argument")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fileRef := undefined
|
|
137
|
+
fileContent := undefined
|
|
138
|
+
inputSchema := undefined
|
|
139
|
+
inputXsvType := undefined
|
|
140
|
+
finalDataFrameId := undefined
|
|
141
|
+
finalFileName := undefined
|
|
142
|
+
finalXsvType := undefined
|
|
143
|
+
inferSchemaOpt := undefined
|
|
144
|
+
|
|
145
|
+
isStructuralInput := is_map(frameInput) && !is_undefined(frameInput.file)
|
|
146
|
+
|
|
147
|
+
if isStructuralInput {
|
|
148
|
+
fileRef = frameInput.file
|
|
149
|
+
inputXsvType = frameInput.xsvType
|
|
150
|
+
|
|
151
|
+
if !is_undefined(frameInput.schema) {
|
|
152
|
+
inputSchema = frameInput.schema
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
finalXsvType = inputXsvType
|
|
156
|
+
} else {
|
|
157
|
+
if smart.isReference(frameInput) {
|
|
158
|
+
fileRef = frameInput
|
|
159
|
+
} else {
|
|
160
|
+
fileContent = frameInput
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ll.assert(is_undefined(finalXsvType) || is_undefined(opts.xsvType), "xsvType option cannot be used with structural input as xsvType is already defined in the input structure")
|
|
165
|
+
|
|
166
|
+
if !is_undefined(opts.xsvType) {
|
|
167
|
+
finalXsvType = opts.xsvType
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ll.assert(!is_undefined(finalXsvType), "xsvType option is required for direct file input")
|
|
171
|
+
ll.assert(finalXsvType == "csv" || finalXsvType == "tsv", "xsvType must be 'csv' or 'tsv'")
|
|
172
|
+
|
|
173
|
+
if !is_undefined(opts.id) {
|
|
174
|
+
finalDataFrameId = opts.id
|
|
175
|
+
} else {
|
|
176
|
+
finalDataFrameId = self._newAnonymousDataFrameId()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if !is_undefined(opts.fileName) {
|
|
180
|
+
finalFileName = opts.fileName
|
|
181
|
+
} else {
|
|
182
|
+
finalFileName = self._newAnonymousFileId(finalXsvType)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if !is_undefined(opts.inferSchema) {
|
|
187
|
+
if !is_bool(opts.inferSchema) {
|
|
188
|
+
ll.panic("'inferSchema' option must be a boolean. Got: %T", opts.inferSchema)
|
|
189
|
+
}
|
|
190
|
+
inferSchemaOpt = opts.inferSchema
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if !is_undefined(fileRef) && !smart.isReference(fileRef) {
|
|
194
|
+
ll.panic("frameInput.file (for structural input) or frameInput (for direct reference) must be a valid resource reference. Got: %v", fileRef)
|
|
195
|
+
} else if !is_undefined(fileContent) && !is_string(fileContent) {
|
|
196
|
+
ll.panic("frameInput (for direct content) must be a string. Got: %v", fileContent)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if !is_undefined(fileRef) {
|
|
200
|
+
inFiles = append(inFiles, { file: fileRef, name: finalFileName })
|
|
201
|
+
} else {
|
|
202
|
+
inFiles = append(inFiles, { content: fileContent, name: finalFileName })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
readCsvStep := {
|
|
206
|
+
type: "read_csv",
|
|
207
|
+
file: finalFileName,
|
|
208
|
+
name: finalDataFrameId
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if finalXsvType == "csv" {
|
|
212
|
+
readCsvStep.delimiter = ","
|
|
213
|
+
} else if finalXsvType == "tsv" {
|
|
214
|
+
readCsvStep.delimiter = "\t"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if !is_undefined(inputSchema) {
|
|
218
|
+
readCsvStep.schema = inputSchema
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if !is_undefined(inferSchemaOpt) && inferSchemaOpt == false {
|
|
224
|
+
readCsvStep.infer_schema = false
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
self.addRawStep(readCsvStep)
|
|
228
|
+
return _newDataFrame(self, finalDataFrameId)
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
_newAnonymousDataFrameId: func() {
|
|
232
|
+
anonymousDFCounter += 1
|
|
233
|
+
return "anonymous_" + string(anonymousDFCounter)
|
|
234
|
+
},
|
|
235
|
+
_newAnonymousFileId: func(extension) {
|
|
236
|
+
anonymousFilesCounter += 1
|
|
237
|
+
return "anonymous_" + string(anonymousFilesCounter) + "." + extension
|
|
238
|
+
},
|
|
239
|
+
_saveFile: func(name) {
|
|
240
|
+
outFiles = append(outFiles, name)
|
|
241
|
+
},
|
|
242
|
+
_saveFileContent: func(name) {
|
|
243
|
+
outContentFiles = append(outContentFiles, name)
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
run: func() {
|
|
253
|
+
ptablerSw := assets.importSoftware("@platforma-open/milaboratories.software-ptabler:main")
|
|
254
|
+
ptablerCmdBuilder := exec.builder().
|
|
255
|
+
inMediumQueue().
|
|
256
|
+
printErrStreamToStdout().
|
|
257
|
+
dontSaveStdoutOrStderr().
|
|
258
|
+
software(ptablerSw).
|
|
259
|
+
arg("workflow_sc.json").
|
|
260
|
+
writeFile("workflow_sc.json", json.encode({
|
|
261
|
+
workflow: steps
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
for inFile in inFiles {
|
|
265
|
+
if !is_undefined(inFile.file) {
|
|
266
|
+
ptablerCmdBuilder.addFile(inFile.name, inFile.file)
|
|
267
|
+
} else {
|
|
268
|
+
ptablerCmdBuilder.writeFile(inFile.name, inFile.content)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for outFile in outFiles {
|
|
273
|
+
ptablerCmdBuilder.saveFile(outFile)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for outContentFile in outContentFiles {
|
|
277
|
+
ptablerCmdBuilder.saveFileContent(outContentFile)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return ptablerCmdBuilder.run()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return ll.toStrict(self)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_newDataFrameGroupBy = func(parentWorkflow, dfName, groupByExpressions) {
|
|
288
|
+
self := undefined
|
|
289
|
+
self = ll.toStrict({
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
agg: func(...aggExpressions) {
|
|
307
|
+
ll.assert(len(aggExpressions) > 0, "agg method requires at least one aggregation expression.")
|
|
308
|
+
|
|
309
|
+
aggregations := slices.map(aggExpressions, func(aggExpr) {
|
|
310
|
+
if !exp._isAggregation(aggExpr) {
|
|
311
|
+
ll.panic("Invalid argument to agg: Expected an aggregation expression object, got %v", aggExpr)
|
|
312
|
+
}
|
|
313
|
+
return aggExpr.getAggregation()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
317
|
+
parentWorkflow.addRawStep({
|
|
318
|
+
type: "aggregate",
|
|
319
|
+
inputTable: dfName,
|
|
320
|
+
outputTable: outputDfName,
|
|
321
|
+
groupBy: exp._mapToExpressionStructList(groupByExpressions, "col"),
|
|
322
|
+
aggregations: aggregations
|
|
323
|
+
})
|
|
324
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
return self
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
_newDataFrame = func(parentWorkflow, dfName) {
|
|
331
|
+
_mapExprsToStepCols := func(exprs, methodName) {
|
|
332
|
+
ll.assert(len(exprs) > 0, methodName + " requires at least one expression argument.")
|
|
333
|
+
cols := []
|
|
334
|
+
for expr in exprs {
|
|
335
|
+
if !exp._isExpression(expr) {
|
|
336
|
+
ll.panic("Invalid argument to " + methodName + ": Expected an expression object, got %v", expr)
|
|
337
|
+
}
|
|
338
|
+
cols = append(cols, {
|
|
339
|
+
name: expr.getAlias(),
|
|
340
|
+
expression: expr.getExpression()
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
return cols
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
_addSaveStep := func(outputFile, ...options) {
|
|
347
|
+
opts := {}
|
|
348
|
+
if len(options) > 0 {
|
|
349
|
+
opts = options[0]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
delimiter := undefined
|
|
353
|
+
if text.has_suffix(outputFile, ".csv") {
|
|
354
|
+
delimiter = ","
|
|
355
|
+
} else if text.has_suffix(outputFile, ".tsv") {
|
|
356
|
+
delimiter = "\t"
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if !is_undefined(opts.xsvType) {
|
|
360
|
+
if opts.xsvType == "csv" {
|
|
361
|
+
delimiter = ","
|
|
362
|
+
} else if opts.xsvType == "tsv" {
|
|
363
|
+
delimiter = "\t"
|
|
364
|
+
} else {
|
|
365
|
+
ll.panic("Unsupported xsvType: %v", opts.xsvType)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if is_undefined(delimiter) {
|
|
370
|
+
ll.panic("Can't infer xsvType from outputFile extension, and xsvType is not specified in options")
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
step := {
|
|
374
|
+
type: "write_csv",
|
|
375
|
+
table: dfName,
|
|
376
|
+
file: outputFile,
|
|
377
|
+
delimiter: delimiter
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if !is_undefined(opts.columns) {
|
|
381
|
+
step.columns = opts.columns
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
parentWorkflow.addRawStep(step)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
self := undefined
|
|
388
|
+
|
|
389
|
+
self = ll.toStrict({
|
|
390
|
+
_getWorkflow: func() {
|
|
391
|
+
return parentWorkflow
|
|
392
|
+
},
|
|
393
|
+
_getDfName: func() {
|
|
394
|
+
return dfName
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
withColumns: func(...expressions) {
|
|
411
|
+
stepCols := _mapExprsToStepCols(expressions, "withColumns")
|
|
412
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
413
|
+
parentWorkflow.addRawStep({
|
|
414
|
+
type: "with_columns",
|
|
415
|
+
inputTable: dfName,
|
|
416
|
+
outputTable: outputDfName,
|
|
417
|
+
columns: stepCols
|
|
418
|
+
})
|
|
419
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
select: func(...expressions) {
|
|
436
|
+
stepCols := _mapExprsToStepCols(expressions, "select")
|
|
437
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
438
|
+
parentWorkflow.addRawStep({
|
|
439
|
+
type: "select",
|
|
440
|
+
inputTable: dfName,
|
|
441
|
+
outputTable: outputDfName,
|
|
442
|
+
columns: stepCols
|
|
443
|
+
})
|
|
444
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
addColumns: func(...expressions) {
|
|
461
|
+
stepCols := _mapExprsToStepCols(expressions, "addColumns")
|
|
462
|
+
parentWorkflow.addRawStep({
|
|
463
|
+
type: "add_columns",
|
|
464
|
+
table: dfName,
|
|
465
|
+
columns: stepCols
|
|
466
|
+
})
|
|
467
|
+
return self
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
save: func(outputFile, ...options) {
|
|
484
|
+
_addSaveStep(outputFile, options...)
|
|
485
|
+
|
|
486
|
+
parentWorkflow._saveFile(outputFile)
|
|
487
|
+
|
|
488
|
+
return self
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
saveContent: func(outputFile, ...options) {
|
|
507
|
+
_addSaveStep(outputFile, options...)
|
|
508
|
+
|
|
509
|
+
parentWorkflow._saveFileContent(outputFile)
|
|
510
|
+
|
|
511
|
+
return self
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
groupBy: func(...expressions) {
|
|
525
|
+
ll.assert(len(expressions) > 0, "groupBy requires at least one expression argument.")
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
for expr in expressions {
|
|
529
|
+
if !is_string(expr) && !exp._isExpression(expr) {
|
|
530
|
+
ll.panic("Invalid argument to groupBy: Expected a column name (string) or an expression object, got %v", expr)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return _newDataFrameGroupBy(parentWorkflow, dfName, expressions)
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
filter: func(...predicates) {
|
|
549
|
+
ll.assert(len(predicates) > 0, "filter method requires at least one predicate expression.")
|
|
550
|
+
|
|
551
|
+
allConditions := []
|
|
552
|
+
for p in predicates {
|
|
553
|
+
if !exp._isExpression(p) {
|
|
554
|
+
ll.panic("Invalid argument in filter: Expected an expression object, got %v", p)
|
|
555
|
+
}
|
|
556
|
+
allConditions = append(allConditions, p)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
condition := undefined
|
|
560
|
+
if len(allConditions) == 1 {
|
|
561
|
+
condition = allConditions[0]
|
|
562
|
+
} else {
|
|
563
|
+
condition = exp.and(allConditions...)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
567
|
+
parentWorkflow.addRawStep({
|
|
568
|
+
type: "filter",
|
|
569
|
+
inputTable: dfName,
|
|
570
|
+
outputTable: outputDfName,
|
|
571
|
+
condition: condition.getExpression()
|
|
572
|
+
})
|
|
573
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
sort: func(by, ...optionsRaw) {
|
|
602
|
+
ll.assert(is_array(by), "First argument to sort must be an array of sort keys (column names or expressions).")
|
|
603
|
+
ll.assert(len(by) > 0, "Sort keys array cannot be empty.")
|
|
604
|
+
|
|
605
|
+
opts := {}
|
|
606
|
+
if len(optionsRaw) > 0 {
|
|
607
|
+
ll.assert(len(optionsRaw) == 1, "sort expects at most one options map argument after the sort keys array.")
|
|
608
|
+
if !is_map(optionsRaw[0]) {
|
|
609
|
+
ll.panic("Second argument to sort (if provided) must be an options map. Got: %T", optionsRaw[0])
|
|
610
|
+
}
|
|
611
|
+
opts = optionsRaw[0]
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
descendingOpt := opts.descending
|
|
615
|
+
nullsLastOpt := opts.nulls_last
|
|
616
|
+
maintainOrderOpt := opts.maintain_order // Corresponds to 'stable' in PTabler SortStep
|
|
617
|
+
|
|
618
|
+
if !is_undefined(descendingOpt) && !is_bool(descendingOpt) && !is_array(descendingOpt) {
|
|
619
|
+
ll.panic("sort 'descending' option must be a boolean or an array of booleans. Got: %T", descendingOpt)
|
|
620
|
+
}
|
|
621
|
+
if is_array(descendingOpt) && len(descendingOpt) != len(by) {
|
|
622
|
+
ll.panic("sort 'descending' array length (%d) must match the number of sort keys (%d).", len(descendingOpt), len(by))
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if !is_undefined(nullsLastOpt) && !is_bool(nullsLastOpt) && !is_array(nullsLastOpt) {
|
|
626
|
+
ll.panic("sort 'nulls_last' option must be a boolean or an array of booleans. Got: %T", nullsLastOpt)
|
|
627
|
+
}
|
|
628
|
+
if is_array(nullsLastOpt) && len(nullsLastOpt) != len(by) {
|
|
629
|
+
ll.panic("sort 'nulls_last' array length (%d) must match the number of sort keys (%d).", len(nullsLastOpt), len(by))
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if !is_undefined(maintainOrderOpt) && !is_bool(maintainOrderOpt) {
|
|
633
|
+
ll.panic("sort 'maintain_order' option must be a boolean. Got: %T", maintainOrderOpt)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
sortDirectives := []
|
|
637
|
+
for i, item in by {
|
|
638
|
+
directive := { value: undefined }
|
|
639
|
+
|
|
640
|
+
if is_string(item) {
|
|
641
|
+
directive.value = exp.col(item).getExpression()
|
|
642
|
+
} else if exp._isExpression(item) {
|
|
643
|
+
directive.value = item.getExpression()
|
|
644
|
+
} else {
|
|
645
|
+
ll.panic("Invalid sort key at index %d in array: Expected a column name (string) or an expression object, got %T", i, item)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
itemDesc := false // Polars default
|
|
650
|
+
if is_array(descendingOpt) {
|
|
651
|
+
val := descendingOpt[i]
|
|
652
|
+
ll.assert(is_bool(val), "Elements of 'descending' array (at index %d) must be booleans. Got: %T", i, val)
|
|
653
|
+
itemDesc = val
|
|
654
|
+
} else if is_bool(descendingOpt) {
|
|
655
|
+
itemDesc = descendingOpt
|
|
656
|
+
}
|
|
657
|
+
if itemDesc { // Only include if true, as PTabler 'descending' defaults to false
|
|
658
|
+
directive.descending = true
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
itemNullsLast := undefined // Let PTabler use its default if not specified
|
|
663
|
+
if is_array(nullsLastOpt) {
|
|
664
|
+
val := nullsLastOpt[i]
|
|
665
|
+
ll.assert(is_bool(val), "Elements of 'nulls_last' array (at index %d) must be booleans. Got: %T", i, val)
|
|
666
|
+
itemNullsLast = val
|
|
667
|
+
} else if is_bool(nullsLastOpt) {
|
|
668
|
+
itemNullsLast = nullsLastOpt
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if !is_undefined(itemNullsLast) { // Only include if explicitly set
|
|
672
|
+
directive.nullsLast = itemNullsLast
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
sortDirectives = append(sortDirectives, directive)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
679
|
+
|
|
680
|
+
sortStep := {
|
|
681
|
+
type: "sort",
|
|
682
|
+
inputTable: dfName,
|
|
683
|
+
outputTable: outputDfName,
|
|
684
|
+
by: sortDirectives
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if !is_undefined(maintainOrderOpt) && maintainOrderOpt == true {
|
|
688
|
+
sortStep.stable = true // PTabler 'stable' maps to Polars 'maintain_order'
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
parentWorkflow.addRawStep(sortStep)
|
|
692
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
join: func(rightDf, opts) {
|
|
728
|
+
if is_undefined(opts) || !is_map(opts) {
|
|
729
|
+
ll.panic("join options must be a map argument.")
|
|
730
|
+
}
|
|
731
|
+
if is_undefined(rightDf) || is_undefined(rightDf._getDfName) || is_undefined(rightDf._getWorkflow) {
|
|
732
|
+
ll.panic("rightDf argument for join must be a DataFrame object created by pt.workflow().frame().")
|
|
733
|
+
}
|
|
734
|
+
if rightDf._getWorkflow()._wfId != parentWorkflow._wfId {
|
|
735
|
+
ll.panic("Both DataFrames in a join operation must belong to the same workflow.")
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
_ensureArrayLocal := func(val, argName) {
|
|
739
|
+
if is_undefined(val) {
|
|
740
|
+
return undefined
|
|
741
|
+
}
|
|
742
|
+
if is_string(val) {
|
|
743
|
+
return [val]
|
|
744
|
+
}
|
|
745
|
+
if is_array(val) {
|
|
746
|
+
for i, item in val {
|
|
747
|
+
if !is_string(item) {
|
|
748
|
+
ll.panic("'%s' option: if an array, all elements must be strings. Found type %T at index %d.", argName, item, i)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return val
|
|
752
|
+
}
|
|
753
|
+
ll.panic("'%s' option must be a string or an array of strings. Got: %T", argName, val)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
_validateAndSetColumnsLocal := func(columnsOpt, optName) {
|
|
757
|
+
if is_undefined(columnsOpt) {
|
|
758
|
+
return undefined
|
|
759
|
+
}
|
|
760
|
+
if !is_array(columnsOpt) {
|
|
761
|
+
ll.panic("'%s' option must be an array of column mappings or strings. Got: %T", optName, columnsOpt)
|
|
762
|
+
}
|
|
763
|
+
mappings := []
|
|
764
|
+
for i, item in columnsOpt {
|
|
765
|
+
entry := undefined
|
|
766
|
+
if is_string(item) {
|
|
767
|
+
entry = { column: item }
|
|
768
|
+
} else if is_map(item) {
|
|
769
|
+
if is_undefined(item.column) || !is_string(item.column) {
|
|
770
|
+
ll.panic("Each map mapping in '%s' at index %d must have a 'column' string field. Got: %v", optName, i, item.column)
|
|
771
|
+
}
|
|
772
|
+
entry = { column: item.column }
|
|
773
|
+
if !is_undefined(item.rename) {
|
|
774
|
+
if !is_string(item.rename) {
|
|
775
|
+
ll.panic("The 'rename' field in '%s' mapping at index %d must be a string. Got: %T", optName, i, item.rename)
|
|
776
|
+
}
|
|
777
|
+
entry.rename = item.rename
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
ll.panic("Each element in '%s' at index %d must be a string or a map. Got: %T", optName, i, item)
|
|
781
|
+
}
|
|
782
|
+
mappings = append(mappings, entry)
|
|
783
|
+
}
|
|
784
|
+
if len(mappings) == 0 { // PTabler schema might not like empty arrays for this.
|
|
785
|
+
return undefined
|
|
786
|
+
}
|
|
787
|
+
return mappings
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
how := opts.how
|
|
791
|
+
if is_undefined(how) {
|
|
792
|
+
how = "inner" // Default join type
|
|
793
|
+
}
|
|
794
|
+
validHowTypes := ["inner", "left", "right", "full", "cross"]
|
|
795
|
+
if !slices.hasElement(validHowTypes, how) {
|
|
796
|
+
ll.panic("Invalid 'how' value for join: '%s'. Must be one of %v", how, validHowTypes)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
onOpt := _ensureArrayLocal(opts.on, "on")
|
|
800
|
+
leftOnOpt := _ensureArrayLocal(opts.leftOn, "leftOn")
|
|
801
|
+
rightOnOpt := _ensureArrayLocal(opts.rightOn, "rightOn")
|
|
802
|
+
|
|
803
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
804
|
+
joinStep := {
|
|
805
|
+
type: "join",
|
|
806
|
+
leftTable: dfName,
|
|
807
|
+
rightTable: rightDf._getDfName(),
|
|
808
|
+
outputTable: outputDfName,
|
|
809
|
+
how: how
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if how == "cross" {
|
|
813
|
+
if !is_undefined(onOpt) || !is_undefined(leftOnOpt) || !is_undefined(rightOnOpt) {
|
|
814
|
+
ll.panic("'on', 'leftOn', 'rightOn' options cannot be used with 'cross' join.")
|
|
815
|
+
}
|
|
816
|
+
if !is_undefined(opts.coalesce) {
|
|
817
|
+
ll.panic("'coalesce' option cannot be used with 'cross' join.")
|
|
818
|
+
}
|
|
819
|
+
} else { // 'inner', 'left', 'right', 'full'
|
|
820
|
+
if !is_undefined(onOpt) {
|
|
821
|
+
if !is_undefined(leftOnOpt) || !is_undefined(rightOnOpt) {
|
|
822
|
+
ll.panic("For '%s' join, you must specify either 'on' option or both 'leftOn' and 'rightOn' options. 'on' was provided, but so was 'leftOn' or 'rightOn'.", how)
|
|
823
|
+
}
|
|
824
|
+
ll.assert(len(onOpt) > 0, "'on' option, if provided, cannot be an empty list.")
|
|
825
|
+
joinStep.leftOn = onOpt
|
|
826
|
+
joinStep.rightOn = onOpt
|
|
827
|
+
} else if !is_undefined(leftOnOpt) && !is_undefined(rightOnOpt) {
|
|
828
|
+
ll.assert(len(leftOnOpt) > 0, "'leftOn' option, if provided, cannot be an empty list.")
|
|
829
|
+
ll.assert(len(rightOnOpt) > 0, "'rightOn' option, if provided, cannot be an empty list.")
|
|
830
|
+
if len(leftOnOpt) != len(rightOnOpt) {
|
|
831
|
+
ll.panic("'leftOn' and 'rightOn' lists must have the same number of columns. Got %d and %d.", len(leftOnOpt), len(rightOnOpt))
|
|
832
|
+
}
|
|
833
|
+
joinStep.leftOn = leftOnOpt
|
|
834
|
+
joinStep.rightOn = rightOnOpt
|
|
835
|
+
} else {
|
|
836
|
+
ll.panic("For '%s' join, you must specify either 'on' option or both 'leftOn' and 'rightOn' options.", how)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if !is_undefined(opts.coalesce) {
|
|
840
|
+
if !is_bool(opts.coalesce) {
|
|
841
|
+
ll.panic("'coalesce' option must be a boolean. Got: %T", opts.coalesce)
|
|
842
|
+
}
|
|
843
|
+
joinStep.coalesce = opts.coalesce
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
leftCols := _validateAndSetColumnsLocal(opts.leftColumns, "leftColumns")
|
|
848
|
+
if !is_undefined(leftCols) {
|
|
849
|
+
joinStep.leftColumns = leftCols
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
rightCols := _validateAndSetColumnsLocal(opts.rightColumns, "rightColumns")
|
|
853
|
+
if !is_undefined(rightCols) {
|
|
854
|
+
joinStep.rightColumns = rightCols
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
parentWorkflow.addRawStep(joinStep)
|
|
858
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
withoutColumns: func(...columns) {
|
|
870
|
+
ll.assert(is_array(columns), "Argument to withoutColumns must be an array of column names.")
|
|
871
|
+
ll.assert(len(columns) > 0, "Columns array for withoutColumns cannot be empty.")
|
|
872
|
+
for i, colName in columns {
|
|
873
|
+
ll.assert(is_string(colName), "Each element in the columns array for withoutColumns (at index %d) must be a string. Got: %T", i, colName)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
877
|
+
parentWorkflow.addRawStep({
|
|
878
|
+
type: "without_columns",
|
|
879
|
+
inputTable: dfName,
|
|
880
|
+
outputTable: outputDfName,
|
|
881
|
+
columns: columns
|
|
882
|
+
})
|
|
883
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
884
|
+
}
|
|
885
|
+
})
|
|
886
|
+
return self
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
concat := func(dataframes, ...optionsRaw) {
|
|
912
|
+
ll.assert(is_array(dataframes), "First argument to concat must be an array of DataFrame objects.")
|
|
913
|
+
ll.assert(len(dataframes) > 0, "DataFrame array for concat cannot be empty.")
|
|
914
|
+
|
|
915
|
+
opts := {}
|
|
916
|
+
if len(optionsRaw) > 0 {
|
|
917
|
+
if len(optionsRaw) == 1 && is_map(optionsRaw[0]) {
|
|
918
|
+
opts = optionsRaw[0]
|
|
919
|
+
} else {
|
|
920
|
+
ll.panic("concat options must be a single map argument")
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
parentWorkflow := undefined
|
|
925
|
+
inputTableNames := []
|
|
926
|
+
|
|
927
|
+
for i, df in dataframes {
|
|
928
|
+
if is_undefined(df) || is_undefined(df._getDfName) || is_undefined(df._getWorkflow) {
|
|
929
|
+
ll.panic("Argument at index %d in dataframes array is not a valid DataFrame object.", i)
|
|
930
|
+
}
|
|
931
|
+
currentWorkflow := df._getWorkflow()
|
|
932
|
+
if is_undefined(parentWorkflow) {
|
|
933
|
+
parentWorkflow = currentWorkflow
|
|
934
|
+
} else if currentWorkflow._wfId != parentWorkflow._wfId {
|
|
935
|
+
ll.panic("All DataFrames in a concat operation must belong to the same workflow.")
|
|
936
|
+
}
|
|
937
|
+
inputTableNames = append(inputTableNames, df._getDfName())
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
outputDfName := parentWorkflow._newAnonymousDataFrameId()
|
|
941
|
+
|
|
942
|
+
concatenateStep := {
|
|
943
|
+
type: "concatenate",
|
|
944
|
+
inputTables: inputTableNames,
|
|
945
|
+
outputTable: outputDfName
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if !is_undefined(opts.columns) {
|
|
949
|
+
if !is_array(opts.columns) {
|
|
950
|
+
ll.panic("'columns' option for concat must be an array of strings.")
|
|
951
|
+
}
|
|
952
|
+
for i, colName in opts.columns {
|
|
953
|
+
if !is_string(colName) {
|
|
954
|
+
ll.panic("Each element in 'columns' option array (at index %d) must be a string. Got: %T", i, colName)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
concatenateStep.columns = opts.columns
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
parentWorkflow.addRawStep(concatenateStep)
|
|
961
|
+
return _newDataFrame(parentWorkflow, outputDfName)
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export ll.toStrict({
|
|
965
|
+
workflow: workflow,
|
|
966
|
+
concat: concat,
|
|
967
|
+
|
|
968
|
+
col: exp.col,
|
|
969
|
+
lit: exp.lit,
|
|
970
|
+
concatStr: exp.concatStr,
|
|
971
|
+
minHorizontal: exp.minHorizontal,
|
|
972
|
+
maxHorizontal: exp.maxHorizontal,
|
|
973
|
+
allHorizontal: exp.allHorizontal,
|
|
974
|
+
anyHorizontal: exp.anyHorizontal,
|
|
975
|
+
and: exp.and,
|
|
976
|
+
or: exp.or,
|
|
977
|
+
rank: exp.rank,
|
|
978
|
+
when: exp.when,
|
|
979
|
+
rawExp: exp.rawExp
|
|
980
|
+
})
|