neoorm 0.9.2 → 0.9.5

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.
@@ -246,48 +246,570 @@ function formatTsValue(kind, value) {
246
246
  }
247
247
  return JSON.stringify(value);
248
248
  }
249
- function parseEqualityWhere(whereSql, tsNameBySql) {
250
- const parts = whereSql.split(/\s+AND\s+/i);
251
- const fields = [];
252
- for (const raw of parts) {
253
- const part = raw
254
- .trim()
255
- .replace(/^\(|\)$/g, "")
256
- .trim();
257
- const isNull = part.match(/^"?([A-Za-z_][\w]*)"?\s+IS NULL$/i);
258
- if (isNull?.[1]) {
259
- const ts = tsNameBySql.get(isNull[1]) ?? isNull[1];
260
- fields.push(`${ts}: null`);
249
+ function stripOuterParens(sql) {
250
+ let text = sql.trim();
251
+ for (;;) {
252
+ if (!text.startsWith("(") || !text.endsWith(")"))
253
+ return text;
254
+ let depth = 0;
255
+ let balanced = true;
256
+ let inStr = false;
257
+ for (let i = 0; i < text.length; i++) {
258
+ const ch = text[i];
259
+ if (inStr) {
260
+ if (ch === "'") {
261
+ if (text[i + 1] === "'") {
262
+ i++;
263
+ }
264
+ else {
265
+ inStr = false;
266
+ }
267
+ }
268
+ continue;
269
+ }
270
+ if (ch === "'") {
271
+ inStr = true;
272
+ }
273
+ else if (ch === "(") {
274
+ depth++;
275
+ }
276
+ else if (ch === ")") {
277
+ depth--;
278
+ if (depth === 0 && i !== text.length - 1) {
279
+ balanced = false;
280
+ break;
281
+ }
282
+ }
283
+ }
284
+ if (!balanced || depth !== 0)
285
+ return text;
286
+ text = text.slice(1, -1).trim();
287
+ }
288
+ }
289
+ function splitTopLevel(sql, keyword) {
290
+ const parts = [];
291
+ let depth = 0;
292
+ let inStr = false;
293
+ let current = "";
294
+ const upper = sql.toUpperCase();
295
+ let i = 0;
296
+ while (i < sql.length) {
297
+ const ch = sql[i];
298
+ if (inStr) {
299
+ current += ch;
300
+ if (ch === "'") {
301
+ if (sql[i + 1] === "'") {
302
+ current += "'";
303
+ i += 2;
304
+ continue;
305
+ }
306
+ inStr = false;
307
+ }
308
+ i++;
309
+ continue;
310
+ }
311
+ if (ch === "'") {
312
+ inStr = true;
313
+ current += ch;
314
+ i++;
261
315
  continue;
262
316
  }
263
- const eq = part.match(/^"?([A-Za-z_][\w]*)"?\s*=\s*(.+)$/);
264
- if (!eq?.[1] || eq[2] === undefined) {
317
+ if (ch === "(") {
318
+ depth++;
319
+ current += ch;
320
+ i++;
321
+ continue;
322
+ }
323
+ if (ch === ")") {
324
+ depth--;
325
+ current += ch;
326
+ i++;
327
+ continue;
328
+ }
329
+ if (depth === 0) {
330
+ const rest = upper.slice(i);
331
+ const match = rest.match(/^(AND|OR)\b/);
332
+ if (match?.[1] === keyword) {
333
+ const before = sql[i - 1];
334
+ if (before === undefined ||
335
+ /\s/.test(before) ||
336
+ before === "(" ||
337
+ before === ")") {
338
+ parts.push(current);
339
+ current = "";
340
+ i += keyword.length;
341
+ continue;
342
+ }
343
+ }
344
+ }
345
+ current += ch;
346
+ i++;
347
+ }
348
+ if (inStr || depth !== 0)
349
+ return undefined;
350
+ parts.push(current);
351
+ if (parts.length <= 1)
352
+ return undefined;
353
+ return parts;
354
+ }
355
+ function parseIndexLiteralValue(valueSql) {
356
+ const text = valueSql.trim();
357
+ if (text === "true" || text === "false") {
358
+ return { ok: true, ts: text };
359
+ }
360
+ if (/^-?\d+(\.\d+)?$/.test(text)) {
361
+ return { ok: true, ts: text };
362
+ }
363
+ if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) {
364
+ return {
365
+ ok: true,
366
+ ts: `"${escapeTsString(text.slice(1, -1).replace(/''/g, "'"))}"`,
367
+ };
368
+ }
369
+ if (text.startsWith('"') && text.endsWith('"') && text.length >= 2) {
370
+ return { ok: true, ts: `"${escapeTsString(text.slice(1, -1))}"` };
371
+ }
372
+ return { ok: false };
373
+ }
374
+ function unescapeLikePattern(pattern) {
375
+ let out = "";
376
+ for (let i = 0; i < pattern.length; i++) {
377
+ const ch = pattern[i];
378
+ if (ch === "\\") {
379
+ const next = pattern[i + 1];
380
+ if (next === "%" || next === "_" || next === "\\") {
381
+ out += next;
382
+ i++;
383
+ continue;
384
+ }
265
385
  return undefined;
266
386
  }
267
- const ts = tsNameBySql.get(eq[1]) ?? eq[1];
268
- const valueSql = eq[2].trim();
269
- let value;
270
- if (valueSql === "true" || valueSql === "false") {
271
- value = valueSql;
387
+ if (ch === "%" || ch === "_")
388
+ return undefined;
389
+ out += ch;
390
+ }
391
+ return out;
392
+ }
393
+ function likePatternToOp(pattern) {
394
+ const starts = pattern.startsWith("%");
395
+ const ends = pattern.endsWith("%");
396
+ const inner = pattern.slice(starts ? 1 : 0, ends ? pattern.length - 1 : pattern.length);
397
+ if (inner.includes("%"))
398
+ return undefined;
399
+ const value = unescapeLikePattern(inner);
400
+ if (value === undefined)
401
+ return undefined;
402
+ if (starts && ends)
403
+ return { op: "contains", value };
404
+ if (ends)
405
+ return { op: "startsWith", value };
406
+ if (starts)
407
+ return { op: "endsWith", value };
408
+ return undefined;
409
+ }
410
+ const INDEX_ATOM_COL = String.raw `(?:"(?<q1>[^"]+)"|(?<q2>[A-Za-z_][\w]*))`;
411
+ function parseIndexAtom(atom, tsNameBySql) {
412
+ const text = stripOuterParens(atom);
413
+ const notMatch = text.match(/^NOT\b\s*([\s\S]*)$/i);
414
+ if (notMatch?.[1]) {
415
+ const inner = parseIndexConjunction(notMatch[1], tsNameBySql);
416
+ if (!inner)
417
+ return undefined;
418
+ return { kind: "not", item: inner };
419
+ }
420
+ const tsOf = (sql) => {
421
+ if (sql === undefined)
422
+ return undefined;
423
+ return tsNameBySql.get(sql) ?? sql;
424
+ };
425
+ let match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s+IS\\s+NOT\\s+NULL$`, "i"));
426
+ if (match) {
427
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
428
+ if (!ts)
429
+ return undefined;
430
+ return {
431
+ kind: "col",
432
+ ts,
433
+ expr: "{ isNotNull: true }",
434
+ ops: { isNotNull: "true" },
435
+ };
436
+ }
437
+ match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s+IS\\s+NULL$`, "i"));
438
+ if (match) {
439
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
440
+ if (!ts)
441
+ return undefined;
442
+ return { kind: "col", ts, expr: "null" };
443
+ }
444
+ match = text.match(new RegExp(`^LOWER\\(${INDEX_ATOM_COL}\\)\\s*=\\s*LOWER\\((?<lit>[\\s\\S]+)\\)$`, "i"));
445
+ if (match) {
446
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
447
+ const lit = match.groups?.lit
448
+ ? parseIndexLiteralValue(match.groups.lit)
449
+ : { ok: false };
450
+ if (!ts || !lit.ok)
451
+ return undefined;
452
+ return {
453
+ kind: "col",
454
+ ts,
455
+ expr: `{ equals: ${lit.ts}, mode: "insensitive" }`,
456
+ ops: { equals: lit.ts, mode: '"insensitive"' },
457
+ };
458
+ }
459
+ match = text.match(new RegExp(`^LOWER\\(${INDEX_ATOM_COL}\\)\\s+LIKE\\s+LOWER\\((?<lit>[\\s\\S]+?)\\)(?:\\s+ESCAPE\\s+'(?:\\\\|\\\\\\\\)')?$`, "i"));
460
+ if (match) {
461
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
462
+ const lit = match.groups?.lit
463
+ ? parseIndexLiteralValue(match.groups.lit)
464
+ : { ok: false };
465
+ if (!ts || !lit.ok)
466
+ return undefined;
467
+ const raw = match.groups?.lit?.trim() ?? "";
468
+ const inner = raw.startsWith("'")
469
+ ? raw.slice(1, -1).replace(/''/g, "'")
470
+ : undefined;
471
+ if (inner === undefined)
472
+ return undefined;
473
+ const mapped = likePatternToOp(inner);
474
+ if (!mapped)
475
+ return undefined;
476
+ return {
477
+ kind: "col",
478
+ ts,
479
+ expr: `{ ${mapped.op}: "${escapeTsString(mapped.value)}", mode: "insensitive" }`,
480
+ ops: {
481
+ [mapped.op]: `"${escapeTsString(mapped.value)}"`,
482
+ mode: '"insensitive"',
483
+ },
484
+ };
485
+ }
486
+ match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s+NOT\\s+IN\\s*\\((?<list>[\\s\\S]+)\\)$`, "i"));
487
+ if (match) {
488
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
489
+ const list = parseIndexLiteralList(match.groups?.list ?? "");
490
+ if (!ts || !list)
491
+ return undefined;
492
+ return {
493
+ kind: "col",
494
+ ts,
495
+ expr: `{ notIn: [${list.join(", ")}] }`,
496
+ ops: { notIn: `[${list.join(", ")}]` },
497
+ };
498
+ }
499
+ match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s+IN\\s*\\((?<list>[\\s\\S]+)\\)$`, "i"));
500
+ if (match) {
501
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
502
+ const list = parseIndexLiteralList(match.groups?.list ?? "");
503
+ if (!ts || !list)
504
+ return undefined;
505
+ return {
506
+ kind: "col",
507
+ ts,
508
+ expr: `{ in: [${list.join(", ")}] }`,
509
+ ops: { in: `[${list.join(", ")}]` },
510
+ };
511
+ }
512
+ match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s+LIKE\\s+(?<lit>'(?:[^']|'')*')\\s*(?:ESCAPE\\s+'(?:\\\\|\\\\\\\\)')?$`, "i"));
513
+ if (match) {
514
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
515
+ const lit = match.groups?.lit
516
+ ? parseIndexLiteralValue(match.groups.lit)
517
+ : { ok: false };
518
+ if (!ts || !lit.ok)
519
+ return undefined;
520
+ const raw = (match.groups?.lit ?? "").slice(1, -1).replace(/''/g, "'");
521
+ const mapped = likePatternToOp(raw);
522
+ if (!mapped)
523
+ return undefined;
524
+ return {
525
+ kind: "col",
526
+ ts,
527
+ expr: `{ ${mapped.op}: "${escapeTsString(mapped.value)}" }`,
528
+ ops: { [mapped.op]: `"${escapeTsString(mapped.value)}"` },
529
+ };
530
+ }
531
+ match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s*(?<op>~\\*|~)\\s*(?<lit>'(?:[^']|'')*')$`, "i"));
532
+ if (match) {
533
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
534
+ const lit = match.groups?.lit
535
+ ? parseIndexLiteralValue(match.groups.lit)
536
+ : { ok: false };
537
+ if (!ts || !lit.ok)
538
+ return undefined;
539
+ const insensitive = match.groups?.op === "~*";
540
+ return {
541
+ kind: "col",
542
+ ts,
543
+ expr: insensitive
544
+ ? `{ search: ${lit.ts}, mode: "insensitive" }`
545
+ : `{ search: ${lit.ts} }`,
546
+ ops: insensitive
547
+ ? { search: lit.ts, mode: '"insensitive"' }
548
+ : { search: lit.ts },
549
+ };
550
+ }
551
+ match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s+REGEXP\\s+(?<lit>'(?:[^']|'')*')$`, "i"));
552
+ if (match) {
553
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
554
+ const lit = match.groups?.lit
555
+ ? parseIndexLiteralValue(match.groups.lit)
556
+ : { ok: false };
557
+ if (!ts || !lit.ok)
558
+ return undefined;
559
+ return {
560
+ kind: "col",
561
+ ts,
562
+ expr: `{ search: ${lit.ts} }`,
563
+ ops: { search: lit.ts },
564
+ };
565
+ }
566
+ match = text.match(new RegExp(`^regexp_i\\((?<lit>'(?:[^']|'')*')\\s*,\\s*${INDEX_ATOM_COL}\\)$`, "i"));
567
+ if (match) {
568
+ const allGroups = match.groups ?? {};
569
+ const colSql = allGroups.q1 ?? allGroups.q2;
570
+ const ts = tsOf(colSql);
571
+ const lit = match.groups?.lit
572
+ ? parseIndexLiteralValue(match.groups.lit)
573
+ : { ok: false };
574
+ if (!ts || !lit.ok)
575
+ return undefined;
576
+ return {
577
+ kind: "col",
578
+ ts,
579
+ expr: `{ search: ${lit.ts}, mode: "insensitive" }`,
580
+ ops: { search: lit.ts, mode: '"insensitive"' },
581
+ };
582
+ }
583
+ match = text.match(new RegExp(`^${INDEX_ATOM_COL}\\s*(?<op>=|<>|!=|>=|<=|>|<)\\s*(?<lit>[\\s\\S]+)$`, "i"));
584
+ if (match) {
585
+ const ts = tsOf(match.groups?.q1 ?? match.groups?.q2);
586
+ const lit = match.groups?.lit
587
+ ? parseIndexLiteralValue(match.groups.lit)
588
+ : { ok: false };
589
+ if (!ts || !lit.ok)
590
+ return undefined;
591
+ const op = match.groups?.op ?? "=";
592
+ if (op === "=") {
593
+ if (lit.ts === "1" || lit.ts === "0") {
594
+ return {
595
+ kind: "col",
596
+ ts,
597
+ expr: lit.ts === "1" ? "true" : "false",
598
+ };
599
+ }
600
+ if (lit.ts === "true" || lit.ts === "false") {
601
+ return { kind: "col", ts, expr: lit.ts };
602
+ }
603
+ return { kind: "col", ts, expr: lit.ts };
604
+ }
605
+ if (op === "<>" || op === "!=") {
606
+ return {
607
+ kind: "not",
608
+ item: { kind: "col", ts, expr: lit.ts },
609
+ };
272
610
  }
273
- else if (valueSql === "1" || valueSql === "0") {
274
- value = valueSql === "1" ? "true" : "false";
611
+ const opName = op === ">" ? "gt" : op === ">=" ? "gte" : op === "<" ? "lt" : "lte";
612
+ return {
613
+ kind: "col",
614
+ ts,
615
+ expr: `{ ${opName}: ${lit.ts} }`,
616
+ ops: { [opName]: lit.ts },
617
+ };
618
+ }
619
+ return undefined;
620
+ }
621
+ function parseIndexLiteralList(listSql) {
622
+ const items = [];
623
+ let depth = 0;
624
+ let inStr = false;
625
+ let current = "";
626
+ for (let i = 0; i < listSql.length; i++) {
627
+ const ch = listSql[i];
628
+ if (inStr) {
629
+ current += ch;
630
+ if (ch === "'") {
631
+ if (listSql[i + 1] === "'") {
632
+ current += "'";
633
+ i++;
634
+ }
635
+ else {
636
+ inStr = false;
637
+ }
638
+ }
639
+ continue;
275
640
  }
276
- else if (/^-?\d+(\.\d+)?$/.test(valueSql)) {
277
- value = valueSql;
641
+ if (ch === "'") {
642
+ inStr = true;
643
+ current += ch;
644
+ continue;
278
645
  }
279
- else if ((valueSql.startsWith("'") && valueSql.endsWith("'")) ||
280
- (valueSql.startsWith('"') && valueSql.endsWith('"'))) {
281
- value = `"${escapeTsString(valueSql.slice(1, -1))}"`;
646
+ if (ch === "(")
647
+ depth++;
648
+ if (ch === ")")
649
+ depth--;
650
+ if (ch === "," && depth === 0) {
651
+ const parsed = parseIndexLiteralValue(current);
652
+ if (!parsed.ok)
653
+ return undefined;
654
+ if (parsed.ts === "1" || parsed.ts === "0")
655
+ return undefined;
656
+ items.push(parsed.ts);
657
+ current = "";
658
+ continue;
282
659
  }
283
- else {
660
+ current += ch;
661
+ }
662
+ if (inStr || depth !== 0)
663
+ return undefined;
664
+ if (current.trim().length > 0) {
665
+ const parsed = parseIndexLiteralValue(current);
666
+ if (!parsed.ok)
667
+ return undefined;
668
+ if (parsed.ts === "1" || parsed.ts === "0")
669
+ return undefined;
670
+ items.push(parsed.ts);
671
+ }
672
+ if (items.length === 0)
673
+ return undefined;
674
+ return items;
675
+ }
676
+ function parseIndexConjunction(sql, tsNameBySql) {
677
+ const text = stripOuterParens(sql);
678
+ const orBranches = splitTopLevel(text, "OR");
679
+ if (orBranches) {
680
+ const items = [];
681
+ for (const branch of orBranches) {
682
+ const parsed = parseIndexConjunction(branch, tsNameBySql);
683
+ if (!parsed)
684
+ return undefined;
685
+ items.push(parsed);
686
+ }
687
+ return { kind: "or", items };
688
+ }
689
+ const andBranches = splitTopLevel(text, "AND");
690
+ if (andBranches) {
691
+ const items = [];
692
+ for (const branch of andBranches) {
693
+ const parsed = parseIndexConjunction(branch, tsNameBySql);
694
+ if (!parsed)
695
+ return undefined;
696
+ items.push(parsed);
697
+ }
698
+ return { kind: "and", items };
699
+ }
700
+ return parseIndexAtom(text, tsNameBySql);
701
+ }
702
+ function renderIndexPredicate(node) {
703
+ switch (node.kind) {
704
+ case "and": {
705
+ const bodies = conjunctBodies(node.items);
706
+ if (!bodies)
707
+ return undefined;
708
+ if (bodies.length === 1)
709
+ return bodies[0];
710
+ return `AND: [${bodies.map((body) => `{ ${body} }`).join(", ")}]`;
711
+ }
712
+ case "or": {
713
+ const elements = [];
714
+ for (const item of node.items) {
715
+ if (item.kind === "and") {
716
+ const bodies = conjunctBodies(item.items);
717
+ if (!bodies)
718
+ return undefined;
719
+ for (const body of bodies)
720
+ elements.push(`{ ${body} }`);
721
+ continue;
722
+ }
723
+ const inner = renderIndexPredicate(item);
724
+ if (!inner)
725
+ return undefined;
726
+ elements.push(`{ ${inner} }`);
727
+ }
728
+ return `OR: [${elements.join(", ")}]`;
729
+ }
730
+ case "not": {
731
+ const inner = renderIndexPredicate(node.item);
732
+ if (!inner)
733
+ return undefined;
734
+ return `NOT: { ${inner} }`;
735
+ }
736
+ case "col":
737
+ return `${node.ts}: ${node.expr}`;
738
+ }
739
+ }
740
+ /** Group an AND level into where-object bodies, merging same-column operators. */
741
+ function conjunctBodies(items) {
742
+ const bodies = [];
743
+ const pending = new Map();
744
+ const order = [];
745
+ const flush = () => {
746
+ if (order.length === 0)
747
+ return;
748
+ bodies.push(order
749
+ .map((ts) => `${ts}: ${pending.get(ts)?.expr ?? ""}`)
750
+ .join(", "));
751
+ pending.clear();
752
+ order.length = 0;
753
+ };
754
+ for (const item of items) {
755
+ if (item.kind === "col") {
756
+ const incomingOps = item.ops ??
757
+ (item.expr === "null"
758
+ ? { equals: "null" }
759
+ : { equals: item.expr });
760
+ const existing = pending.get(item.ts);
761
+ if (!existing) {
762
+ pending.set(item.ts, item.ops
763
+ ? { expr: item.expr, ops: incomingOps }
764
+ : { expr: item.expr, ops: incomingOps });
765
+ order.push(item.ts);
766
+ continue;
767
+ }
768
+ if (!existing.ops)
769
+ return undefined;
770
+ const merged = { ...existing.ops };
771
+ for (const [op, val] of Object.entries(incomingOps)) {
772
+ if (op in merged)
773
+ return undefined;
774
+ merged[op] = val;
775
+ }
776
+ pending.set(item.ts, {
777
+ expr: `{ ${Object.entries(merged)
778
+ .map(([op, val]) => `${op}: ${val}`)
779
+ .join(", ")} }`,
780
+ ops: merged,
781
+ });
782
+ continue;
783
+ }
784
+ flush();
785
+ const rendered = renderIndexPredicate(item);
786
+ if (!rendered)
787
+ return undefined;
788
+ bodies.push(rendered);
789
+ }
790
+ flush();
791
+ return bodies;
792
+ }
793
+ export function parseIndexWhere(whereSql, tsNameBySql) {
794
+ const parsed = parseIndexConjunction(whereSql, tsNameBySql);
795
+ if (!parsed)
796
+ return undefined;
797
+ if (parsed.kind === "col") {
798
+ return `.where({ ${parsed.ts}: ${parsed.expr} })`;
799
+ }
800
+ if (parsed.kind === "and") {
801
+ const bodies = conjunctBodies(parsed.items);
802
+ if (!bodies || bodies.length === 0)
284
803
  return undefined;
804
+ if (bodies.length === 1) {
805
+ return `.where({ ${bodies[0]} })`;
285
806
  }
286
- fields.push(`${ts}: ${value}`);
807
+ return `.where({ AND: [${bodies.map((body) => `{ ${body} }`).join(", ")}] })`;
287
808
  }
288
- if (fields.length === 0)
809
+ const rendered = renderIndexPredicate(parsed);
810
+ if (!rendered)
289
811
  return undefined;
290
- return `.where({ ${fields.join(", ")} })`;
812
+ return `.where({ ${rendered} })`;
291
813
  }
292
814
  function emitTableExtras(table, tsNameBySql, usedBuilders, manifest) {
293
815
  const extras = [];
@@ -325,7 +847,7 @@ function emitTableExtras(table, tsNameBySql, usedBuilders, manifest) {
325
847
  }
326
848
  }
327
849
  if (index.whereSql) {
328
- const where = parseEqualityWhere(index.whereSql, tsNameBySql);
850
+ const where = parseIndexWhere(index.whereSql, tsNameBySql);
329
851
  if (where)
330
852
  extra += where;
331
853
  }