@naanlang/naan 1.1.0 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naanlang/naan",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "author": "Richard C. Zulch",
5
5
  "description": "Naan™ software platform",
6
6
  "main": "./lib/core/naanlib.js",
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2021-2023 by Richard C. Zulch
9
+ * Copyright (c) 2021-2024 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -36,6 +36,7 @@
36
36
  */
37
37
 
38
38
  closure TimeKeys(prefix, baseMS, local timekeys, prelen) {
39
+ global(UUID)
39
40
  if !UUID.proc
40
41
  throw("TimeKeys requires UUID()")
41
42
  timekeys = new(object, this)
@@ -158,6 +159,7 @@ closure TimeKeys(prefix, baseMS, local timekeys, prelen) {
158
159
  */
159
160
 
160
161
  closure DynaConverter(local conv) {
162
+ global()
161
163
  conv = new(object, this)
162
164
 
163
165
  // fieldTypeDynToNaan
@@ -250,7 +252,7 @@ closure DynaConverter(local conv) {
250
252
  if dtype == "BS" || dtype == "SS"
251
253
  dvalue
252
254
  else if dtype == "NS"
253
- new(dvalue).map(Number.parseFloat(item))
255
+ new(dvalue).map(function(item) { Number.parseFloat(item) })
254
256
  else if dtype == "M"
255
257
  recordDynToNaan(dvalue)
256
258
  else if dtype == "L"
@@ -294,12 +296,24 @@ closure DynaConverter(local conv) {
294
296
  // genAttributeNameAdder
295
297
  //
296
298
  // Return a function that, when called repeatedly, adds attribute name definitions to the
297
- // specified parameter block.
299
+ // specified parameter block. You can specify an attribute name that is a nested key expression
300
+ // resulting from makePath() below. This adds the individual components to the
301
+ // ExpressionAttributeNames list, but generates a return string that references nested elements.
298
302
  //
299
303
  closure genAttributeNameAdder(params, local adex) {
300
304
  function attrib(name, local key, data) {
301
- if !string(name)
302
- name = tostring(name) // try to make it a string
305
+ if numeric(name) || name && symbol(name)
306
+ name = tostring(name)
307
+ else if !string(name) || string.trim().length == 0 { // compound or invalid
308
+ if tuple(name) && name.length == 3 {
309
+ if name.0 == `. // quote(a.b) => (`. a b)
310
+ return (strcat(attrib(name.1), ".", attrib(name.2)))
311
+ else if name.0 == `deref && integer(name.2) // quote(a[99]) => (`deref a 99)
312
+ return (strcat(attrib(name.1), "[", attrib(name.2), "]"))
313
+ }
314
+ debuglog("DynaConverter.genAttributeNameAdder: can't make expression from", typeof(name), name)
315
+ name = tostring(name)
316
+ }
303
317
  if !params.ExpressionAttributeNames
304
318
  params.ExpressionAttributeNames = { }
305
319
  if !adex
@@ -313,6 +327,33 @@ closure DynaConverter(local conv) {
313
327
  }
314
328
  }
315
329
 
330
+
331
+ // makePath
332
+ //
333
+ // Make an attribute path expression that genAttributeNameAdder can use to make an evaluated
334
+ // attribute path. Specify a path expression and then list the symbols that should be evaluated,
335
+ // if any. For example:
336
+ // > b=4
337
+ // > makePath(a.b.c, b)
338
+ // $: a.4.c
339
+ //
340
+ conv.makePath = macro makePath args {
341
+ function mkatpa(path, local exp) {
342
+ exp = path.map(function(el) {
343
+ if tuple(el)
344
+ exp = mkatpa(el)
345
+ else if member(el, args)
346
+ car(el)
347
+ else
348
+ el
349
+ })
350
+ if !tuple(exp) || exp.0 != `deref || integer(exp.2)
351
+ exp
352
+ else
353
+ cons(`., cdr(exp)) // use `. for dictionary lookup
354
+ } (pop(args))
355
+ }
356
+
316
357
  // genAttributeValueAdder
317
358
  //
318
359
  // Return a function that, when called repeatedly, adds attribute value definitions to the
@@ -357,7 +398,7 @@ closure DynaConverter(local conv) {
357
398
  //
358
399
  // condition dictionary:
359
400
  // {
360
- // key: <keyname, e.g. partition key name or path tuple> // required
401
+ // key: <keyname, e.g. partition key name or path> // required
361
402
  // "=": <expression> // exclusive of other comparisons
362
403
  // ">": <expression> // exclusive of other comparisons
363
404
  // "<": <expression> // exclusive of other comparisons
@@ -368,9 +409,12 @@ closure DynaConverter(local conv) {
368
409
  // exists: <boolean> // keyname existence matches boolean
369
410
  // }
370
411
  //
412
+ // The key entry above can be a keyname or a path expression resulting from a call to makePath().
413
+ // Please note that array subscripts, which index into lists, must be integers.
414
+ //
371
415
 
372
416
  conv.createKeyExpression = function createKeyExpression(params, conds
373
- local attval, attname, rangename, range, keyname, op, expr, rangex, arg1, arg2) {
417
+ local attval, attname, rangename, ranges, range, keyname, op, expr, rangex, arg1, arg2) {
374
418
 
375
419
  // dupeop
376
420
  //
@@ -389,18 +433,9 @@ closure DynaConverter(local conv) {
389
433
  ranges = [ranges]
390
434
  for range in ranges {
391
435
  rangex = false
392
- if tuple(range.key) {
393
- keyname = []
394
- expr = range.key
395
- while expr
396
- keyname.push(attname(pop(expr)))
397
- keyname = keyname.join(".")
398
- }
399
- else
400
- keyname = tostring(range.key)
401
- if !keyname
436
+ if !range.key
402
437
  return (list(Error("key expression requires key name")))
403
- keyname = attname(keyname)
438
+ keyname = attname(range.key)
404
439
  for `(op, expr) in range {
405
440
  if op == "key"
406
441
  continue
@@ -465,7 +500,7 @@ closure DynaConverter(local conv) {
465
500
  // value is a standard (error, data) tuple where the data is just { ok: true } if no error.
466
501
  //
467
502
  conv.createUpdateExpression = function createUpdateExpression(params, updates,
468
- local attval, attname, output, item) {
503
+ local attval, attname, output, item, key, value) {
469
504
 
470
505
  if params.UpdateExpression
471
506
  return (list(Error("conv.createUpdateExpression: UpdateExpression already exists")))
@@ -519,13 +554,28 @@ closure DynaConverter(local conv) {
519
554
  *
520
555
  * DynamoDB table.
521
556
  *
557
+ * options:
558
+ * {
559
+ * hashKey: <symbol> // hash key symbol
560
+ * rangeKey: <symbol> // range key symbol (requres hash key)
561
+ * }
562
+ *
522
563
  */
523
564
 
524
- closure DynaTable(dyna, tablename, local table) {
565
+ closure DynaTable(dyna, tablename, options, local table) {
566
+ global()
525
567
  table = new(object, this)
526
568
  table.name = tablename
527
569
  if dyna.ycdb
528
570
  table.ycdb = true // Yandex Cloud version of DynamoDB
571
+ if options.hashKey || options.rangeKey {
572
+ if options.hashKey && symbol(options.hashKey) && symbol(options.rangeKey) { // NB: false is a symbol
573
+ table.hashKey = options.hashKey
574
+ if options.rangeKey
575
+ table.rangeKey = options.rangeKey
576
+ } else
577
+ debuglog("DynaTable: invalid (hashKey/rangeKey) option:", tablename, list(options.hashKey, options.rangeKey))
578
+ }
529
579
 
530
580
  // info
531
581
  //
@@ -544,13 +594,25 @@ closure DynaTable(dyna, tablename, local table) {
544
594
  // rangeType: <name> // string | numeric (if exists)
545
595
  // }
546
596
  //
547
- table.info = closure info(local params, pending) {
597
+ // 20240313: Note that the YCDB / AWS DynamoDB SDK combination has been found to occasionally
598
+ // fail to execute the callback within 30 seconds with multiple overlapping info() calls. In the
599
+ // current example three calls to a single table are made from inside asyncArray(), and all of
600
+ // them get hung up here lacking a callback. Unclear whether the cause is the SDK, the lack of
601
+ // using the newer promises API, or the underlying YDB service. The main reason this is called
602
+ // is to discover the hash and range keys, which can be statically specified. The pragmatic
603
+ // resolution is to allow the caller to specify the keys, which is faster and should avoid this
604
+ // particular challenge. But also info() now combines overlapping calls.
605
+ //
606
+ table.info = closure info(local params, pending, result) {
548
607
  if !dyna.aws
549
608
  return (list(Error("DynaTable.info: not logged into AWS")))
550
609
  params = {
551
610
  TableName: tablename
552
611
  }
612
+ if (pending = table.info_pending)
613
+ return (pending.wait()) // use the first call's results
553
614
  pending = new(nonce)
615
+ table.info_pending = pending // combine overlapping info() calls
554
616
  dyna.aws.describeTable(params, function(error, data, local output, key) {
555
617
  if error {
556
618
  dynaTableError = error
@@ -581,7 +643,9 @@ closure DynaTable(dyna, tablename, local table) {
581
643
  }
582
644
  pending.signal(list(error, output))
583
645
  })
584
- pending.wait()
646
+ result = pending.wait()
647
+ table.info_pending = false
648
+ result
585
649
  }
586
650
 
587
651
  // create
@@ -817,7 +881,7 @@ closure DynaTable(dyna, tablename, local table) {
817
881
 
818
882
 
819
883
  table.deleteRecord = closure deleteRecord(hashValue, rangeValue, options,
820
- local params, pending) {
884
+ local params, error, pending) {
821
885
  if preflightError(this)
822
886
  return
823
887
  params = {
@@ -881,7 +945,6 @@ closure DynaTable(dyna, tablename, local table) {
881
945
  // indexHashKey: <string> // name of index hashKey
882
946
  // range: <key-expression> // range of keys to retrieve
883
947
  // filter: <key-expression> // conditions for items to return
884
- // attmap: <dictionary> // map attribute names to #shortcuts
885
948
  // reverse: <boolean> // reverse order
886
949
  // project: <array> // project only specified attributes
887
950
  // paging: <exclusive start key> // for paging; updated each call
@@ -924,8 +987,6 @@ closure DynaTable(dyna, tablename, local table) {
924
987
  if error
925
988
  return (list(Error("table.queryRecords filter:", error)))
926
989
  }
927
- if options.attmap
928
- params.ExpressionAttributeNames = options.attmap
929
990
  if options.reverse
930
991
  params.ScanIndexForward = false
931
992
  if options.project
@@ -955,7 +1016,6 @@ closure DynaTable(dyna, tablename, local table) {
955
1016
  // {
956
1017
  // index: <string> // index name in table
957
1018
  // filter: <key-expression> // conditions for items to return
958
- // attmap: <dictionary> // map attribute names to #shortcuts
959
1019
  // project: <array> // project only specified attributes
960
1020
  // paging: <exclusive start key> // for paging; updated each call
961
1021
  // consistentRead: <boolean> // true to use consistent read
@@ -980,8 +1040,6 @@ closure DynaTable(dyna, tablename, local table) {
980
1040
  if error
981
1041
  return (list(Error("table.scanRecords filter:", error)))
982
1042
  }
983
- if options.attmap
984
- params.ExpressionAttributeNames = attmap
985
1043
  if options.project
986
1044
  params.ProjectionExpression = options.project.join(", ")
987
1045
  if options.paging
@@ -1016,6 +1074,7 @@ closure DynaTable(dyna, tablename, local table) {
1016
1074
  */
1017
1075
 
1018
1076
  closure DynamoDB(local dyna) {
1077
+ global(awsSDK)
1019
1078
  dyna = new(object, this)
1020
1079
  dyna.timeout = 20000 // default 20-second timeout
1021
1080
  dyna.conv = DynaConverter()
@@ -1045,12 +1104,12 @@ closure DynamoDB(local dyna) {
1045
1104
  // table
1046
1105
  //
1047
1106
  // Return a new table access object for the table of the specified name. The table may not
1048
- // yet exist; see DynaTable for methods.
1107
+ // yet exist; see DynaTable for options and methods.
1049
1108
  //
1050
- dyna.table = function table(name) {
1109
+ dyna.table = function table(name, options) {
1051
1110
  if !dyna.aws
1052
1111
  debuglog("DynamoDB.table: database not open:", name)
1053
- DynaTable(dyna, name)
1112
+ DynaTable(dyna, name, options)
1054
1113
  }
1055
1114
 
1056
1115
  // timekeys
@@ -480,7 +480,7 @@ closure dawsTable(database, path, tableOptions, local table) {
480
480
  table.delete = closure delete(doc, callback) {
481
481
  if !callback
482
482
  return (syncAdapter(delete, doc))
483
- if !doc._id || !doc._id.startsWith(table.s3prefix) || !(doc._md5 || doc._id.slice(-1) == "/")
483
+ if !doc._id || !doc._id.startsWith(table.s3prefix) || !(doc.md5 || doc._md5 || doc._id.slice(-1) == "/")
484
484
  return (asyncResult(callback, Error("invalid argument")))
485
485
  database.bucket.delete(doc._id, function(error, resp) {
486
486
  if error
@@ -537,8 +537,8 @@ closure dawsTable(database, path, tableOptions, local table) {
537
537
  database.bucket.get(id, true, function(error, resp) {
538
538
  if error
539
539
  error = Error("md5 failed", error, id, { status: error.status })
540
- else if resp._md5
541
- resp = resp._md5
540
+ else if resp.md5
541
+ resp = resp.md5
542
542
  else
543
543
  resp = ""
544
544
  callback(error, resp)
package/test/harness.nlg CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  loglevel(2);;
21
- Naan.module.chns("Play");;
21
+ Naan.module.chns("Start");;
22
22
 
23
23
 
24
24
  /*
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2017-2023 by Richard C. Zulch
9
+ * Copyright (c) 2017-2024 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -38,7 +38,7 @@ RegisterTestCategory("context", [
38
38
  [ "function setNS(ns) { ns=reverse(ns),sudo(nsactive(reverse(ns, defNS))) };", "setNS" ],
39
39
  [ "x;", "x" ],
40
40
  [ "y;", "y" ],
41
- [ "length(symlist(nsactive(true).0));", "171"],
41
+ [ "length(symlist(nsactive(true).0));", "172"],
42
42
  [ "testvar;", "testvar" ],
43
43
  [ "function setTestvar(v) { testvar=v };", "setTestvar" ],
44
44
  [ "naanvar;", "naanvar" ],
@@ -266,6 +266,7 @@ RegisterTestCategory("Lingo", [
266
266
  [ "vv -= 2;", "3" ],
267
267
  [ "vv *= 2;", "6" ],
268
268
  [ "vv /= 2;", "3" ],
269
+ [ "vv %= 2;", "1" ],
269
270
 
270
271
  [ "oo = {a:3};", "{ a: 3 }" ],
271
272
  [ "oo.a;", "3" ],
@@ -281,6 +282,7 @@ RegisterTestCategory("Lingo", [
281
282
  [ "oo.a -= 2;", "3" ],
282
283
  [ "oo.a *= 2;", "6" ],
283
284
  [ "oo.a /= 2;", "3" ],
285
+ [ "oo.a %= 2;", "1" ],
284
286
 
285
287
  [ "aa = [2,3];", "[2, 3]" ],
286
288
  [ "++aa[1];", "4" ],
@@ -295,6 +297,7 @@ RegisterTestCategory("Lingo", [
295
297
  [ "aa[1] -= 2;", "3" ],
296
298
  [ "aa[1] *= 2;", "6" ],
297
299
  [ "aa[1] /= 2;", "3" ],
300
+ [ "aa[1] %= 2;", "1" ],
298
301
 
299
302
 
300
303
  //