@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.
Files changed (40) 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 +980 -0
  10. package/dist/tengo/lib/tpl.base.lib.tengo +6 -1
  11. package/dist/tengo/tpl/check_network.create_workdir_from_storage.plj.gz +0 -0
  12. package/dist/tengo/tpl/check_network.download_blob.plj.gz +0 -0
  13. package/dist/tengo/tpl/check_network.run_hello_world.plj.gz +0 -0
  14. package/dist/tengo/tpl/check_network.run_hello_world_py.plj.gz +0 -0
  15. package/dist/tengo/tpl/check_network.upload_blob.plj.gz +0 -0
  16. package/dist/tengo/tpl/check_network.upload_template.plj.gz +0 -0
  17. package/dist/tengo/tpl/exec.R.create-renv.plj.gz +0 -0
  18. package/dist/tengo/tpl/exec.exec.plj.gz +0 -0
  19. package/dist/tengo/tpl/exec.monetization_info.plj.gz +0 -0
  20. package/dist/tengo/tpl/exec.monetization_to_json.plj.gz +0 -0
  21. package/dist/tengo/tpl/exec.python.create-venv.plj.gz +0 -0
  22. package/dist/tengo/tpl/exec.python.install-deps.plj.gz +0 -0
  23. package/dist/tengo/tpl/exec.python.venv-bin-path.plj.gz +0 -0
  24. package/dist/tengo/tpl/ll.get-field.plj.gz +0 -0
  25. package/dist/tengo/tpl/pframes.export-multiple-pcolumns.plj.gz +0 -0
  26. package/dist/tengo/tpl/pframes.export-pframe-for-ui.plj.gz +0 -0
  27. package/dist/tengo/tpl/pframes.export-pframe.plj.gz +0 -0
  28. package/dist/tengo/tpl/pframes.export-single-pcolumn.plj.gz +0 -0
  29. package/dist/tengo/tpl/pframes.import-dir.plj.gz +0 -0
  30. package/dist/tengo/tpl/pframes.process-pcolumn-data.plj.gz +0 -0
  31. package/dist/tengo/tpl/pframes.remove-nulls.plj.gz +0 -0
  32. package/dist/tengo/tpl/pframes.slice-data.plj.gz +0 -0
  33. package/dist/tengo/tpl/pframes.xsv-export-pframe.plj.gz +0 -0
  34. package/dist/tengo/tpl/pframes.xsv-import-if-not-null.plj.gz +0 -0
  35. package/dist/tengo/tpl/pframes.xsv-import-map.plj.gz +0 -0
  36. package/dist/tengo/tpl/waiter.waiter.plj.gz +0 -0
  37. package/dist/tengo/tpl/workdir.save.plj.gz +0 -0
  38. package/dist/tengo/tpl/workflow.build-ctx.plj.gz +0 -0
  39. package/dist/tengo/tpl/workflow.query-anchored.plj.gz +0 -0
  40. 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
+ })