@platforma-sdk/workflow-tengo 4.5.3 → 4.6.1

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