@vibeorm/extensions 2.3.2 → 4.0.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.
@@ -84,7 +84,7 @@ async function pgExtensionCheck(params) {
84
84
  }
85
85
 
86
86
  // packages/extensions/src/search/index.ts
87
- import { ParamCollector, getSqlDialect, renderExpr } from "@vibeorm/sql";
87
+ import { ParamCollector, getSqlDialect, renderExpr, renderSqliteFtsUpdateTrigger } from "@vibeorm/sql";
88
88
  var VERSION = "2.0.0-alpha.2";
89
89
  var SCORE_ALIAS = "_vibe_score";
90
90
  var HIGHLIGHT_ALIAS = "_vibe_highlight";
@@ -327,6 +327,17 @@ function readDeclaration(params) {
327
327
  filterFields: declStringArray(params.declaration, "filterFields")
328
328
  };
329
329
  }
330
+ function resolveLanguage(params) {
331
+ const language = params.decl.language ?? params.options["language"] ?? "english";
332
+ if (!isSafeSqlName({ name: language })) {
333
+ throw new VibeError2({
334
+ code: "VIBE_SCHEMA_INVALID",
335
+ message: `search language "${language}" is not a plain text-search config name`,
336
+ meta: { model: params.model.name, language }
337
+ });
338
+ }
339
+ return language;
340
+ }
330
341
  function weightLetters(params) {
331
342
  const { decl } = params;
332
343
  if (decl.weights === undefined || decl.weights.length !== decl.fields.length)
@@ -438,12 +449,12 @@ function searchArtifacts(ctx) {
438
449
  const decl = readDeclaration({ model, declaration });
439
450
  if (decl === null)
440
451
  continue;
452
+ const language = resolveLanguage({ model, decl, options: ctx.options });
441
453
  const table = tableNameOf({ model });
442
454
  const columns = decl.fields.map((field) => columnNameOf({ model, field }));
443
455
  resolveRank({ decl, options: ctx.options, engine, dialect: ctx.dialect });
444
456
  assertBm25Declarations({ decl, engine, dialect: ctx.dialect });
445
457
  if (engine === "tsvector") {
446
- const language = decl.language ?? ctx.options["language"] ?? "english";
447
458
  const { tsvColumn, indexName } = resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
448
459
  const expression = tsvectorExpression({ dialect, model, decl, language });
449
460
  artifacts.push({
@@ -501,6 +512,7 @@ function searchArtifacts(ctx) {
501
512
  const columnList = columns.map((column) => quote(column)).join(", ");
502
513
  const newList = columns.map((column) => `new.${quote(column)}`).join(", ");
503
514
  const oldList = columns.map((column) => `old.${quote(column)}`).join(", ");
515
+ const updateTrigger = renderSqliteFtsUpdateTrigger({ table, ftsTable: fts, columns });
504
516
  artifacts.push({
505
517
  id: `search:${modelName}:fts5-table`,
506
518
  owns: {
@@ -522,10 +534,7 @@ END;`,
522
534
  `CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_ad`)} AFTER DELETE ON ${quote(table)} BEGIN
523
535
  INSERT INTO ${quote(fts)}(${quote(fts)}, rowid, ${columnList}) VALUES ('delete', old.rowid, ${oldList});
524
536
  END;`,
525
- `CREATE TRIGGER IF NOT EXISTS ${quote(`${fts}_au`)} AFTER UPDATE ON ${quote(table)} BEGIN
526
- INSERT INTO ${quote(fts)}(${quote(fts)}, rowid, ${columnList}) VALUES ('delete', old.rowid, ${oldList});
527
- INSERT INTO ${quote(fts)}(rowid, ${columnList}) VALUES (new.rowid, ${newList});
528
- END;`,
537
+ updateTrigger.create,
529
538
  `INSERT INTO ${quote(fts)}(${quote(fts)}) VALUES ('rebuild');`
530
539
  ],
531
540
  drop: [
@@ -533,6 +542,17 @@ END;`,
533
542
  `DROP TRIGGER IF EXISTS ${quote(`${fts}_ad`)};`,
534
543
  `DROP TRIGGER IF EXISTS ${quote(`${fts}_au`)};`
535
544
  ]
545
+ }, {
546
+ id: `search:${modelName}:fts5-update-trigger`,
547
+ owns: { kind: "trigger", table, name: `${fts}_au` },
548
+ expected: {
549
+ identity: { kind: "trigger", table: { namespace: "main", name: table }, name: `${fts}_au` },
550
+ facts: { definition: updateTrigger.definition, type: "trigger" },
551
+ dependencies: [],
552
+ format: "catalog-v2"
553
+ },
554
+ create: [],
555
+ drop: []
536
556
  });
537
557
  continue;
538
558
  }
@@ -563,15 +583,13 @@ function highlightColumn(params) {
563
583
  return field.column;
564
584
  }
565
585
  function shapeHits(params) {
566
- const { rows, strip, highlightsRequested, engineHasHighlights } = params;
586
+ const { rows, highlightsRequested, engineHasHighlights } = params;
567
587
  return rows.map((raw) => {
568
588
  const row = { ...raw };
569
589
  const score = Number(row[SCORE_ALIAS]);
570
590
  const highlight = row[HIGHLIGHT_ALIAS];
571
591
  delete row[SCORE_ALIAS];
572
592
  delete row[HIGHLIGHT_ALIAS];
573
- for (const name of strip)
574
- delete row[name];
575
593
  const highlights = engineHasHighlights && highlightsRequested && highlight !== null && highlight !== undefined ? [String(highlight)] : null;
576
594
  return { row, score, highlights };
577
595
  });
@@ -581,6 +599,7 @@ function createSearchDelegate(config) {
581
599
  const dialect = ctx.dialect;
582
600
  const quote = (name) => dialect.quoteIdent({ name });
583
601
  const t = quote("t");
602
+ const pkColumns = ctx.model === null ? config.pkColumn === undefined || config.pkColumn === "" ? [] : [config.pkColumn] : ctx.model.primaryKey.map((field) => field.columnName);
584
603
  return async (args) => {
585
604
  if (typeof args.query !== "string" || args.query.trim().length === 0) {
586
605
  throw new VibeError2({
@@ -632,7 +651,14 @@ function createSearchDelegate(config) {
632
651
  meta: { table: config.table }
633
652
  });
634
653
  }
635
- const projection = idOnly ? `${t}.${quote(config.pkColumn ?? "")}` : `${t}.*`;
654
+ if (pkColumns.length === 0) {
655
+ throw new VibeError2({
656
+ code: "VIBE_CONFIG",
657
+ message: `search needs primary-key metadata for deterministic ordering: ${config.table} has no model key, or the legacy mount has no explicit pkColumn`,
658
+ meta: { table: config.table }
659
+ });
660
+ }
661
+ const projection = idOnly ? `${t}.${quote(config.pkColumn ?? "")}` : ctx.projection({ qualify: "t" });
636
662
  const collector = new ParamCollector(dialect);
637
663
  const userWhere = (qualify) => {
638
664
  if (args.where === undefined)
@@ -641,13 +667,13 @@ function createSearchDelegate(config) {
641
667
  return expr === undefined ? null : renderExpr({ dialect, expr, collector });
642
668
  };
643
669
  let text;
644
- let strip = [];
645
670
  let engineHasHighlights = true;
646
671
  if (config.engine === "tsvector") {
647
672
  const language = config.language ?? "english";
648
673
  const ranker = config.rank ?? "ts_rank_cd";
649
674
  const tsv = `${t}.${quote(config.tsvColumn ?? "")}`;
650
- const tsquery = () => prefix ? `to_tsquery('${language}', ${collector.add(matchText)})` : `websearch_to_tsquery('${language}', ${collector.add(matchText)})`;
675
+ const queryParameter = dialect.name === "postgres" ? collector.add(matchText) : undefined;
676
+ const tsquery = () => prefix ? `to_tsquery('${language}', ${queryParameter ?? collector.add(matchText)})` : `websearch_to_tsquery('${language}', ${queryParameter ?? collector.add(matchText)})`;
651
677
  text = `SELECT ${projection}, ${ranker}(${tsv}, ${tsquery()}) AS ${quote(SCORE_ALIAS)}`;
652
678
  if (hlColumn !== null) {
653
679
  text += `, ts_headline('${language}', ${t}.${quote(hlColumn)}, ${tsquery()}) AS ${quote(HIGHLIGHT_ALIAS)}`;
@@ -659,8 +685,6 @@ function createSearchDelegate(config) {
659
685
  if (args.minScore !== undefined) {
660
686
  text += ` AND ${ranker}(${tsv}, ${tsquery()}) >= ${collector.add(args.minScore)}`;
661
687
  }
662
- text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
663
- strip = idOnly ? [] : [config.tsvColumn ?? ""];
664
688
  } else if (config.engine === "fts5") {
665
689
  const fts = quote(config.ftsTable ?? "");
666
690
  text = `SELECT ${projection}, -bm25(${fts}) AS ${quote(SCORE_ALIAS)}`;
@@ -676,7 +700,6 @@ function createSearchDelegate(config) {
676
700
  if (args.minScore !== undefined) {
677
701
  text += ` AND -bm25(${fts}) >= ${collector.add(args.minScore)}`;
678
702
  }
679
- text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
680
703
  } else if (config.engine === "fulltext") {
681
704
  engineHasHighlights = false;
682
705
  const mode = prefix ? "IN BOOLEAN MODE" : "IN NATURAL LANGUAGE MODE";
@@ -689,7 +712,6 @@ function createSearchDelegate(config) {
689
712
  if (args.minScore !== undefined) {
690
713
  text += ` AND ${match()} >= ${collector.add(args.minScore)}`;
691
714
  }
692
- text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
693
715
  } else {
694
716
  const pk = `${t}.${quote(config.pkColumn ?? "")}`;
695
717
  const operator = prefix || fuzzy !== undefined ? "&&&" : "@@@";
@@ -709,15 +731,14 @@ function createSearchDelegate(config) {
709
731
  if (args.minScore !== undefined) {
710
732
  text += ` AND paradedb.score(${pk}) >= ${collector.add(args.minScore)}`;
711
733
  }
712
- text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
713
734
  }
735
+ text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC, ${pkColumns.map((column) => `${t}.${quote(column)} ASC`).join(", ")}`;
714
736
  text += ` LIMIT ${take}`;
715
737
  if (skip !== null)
716
738
  text += ` OFFSET ${skip}`;
717
739
  const rows = await ctx.executeRows({ text, values: collector.values, method: "search" });
718
740
  return shapeHits({
719
741
  rows,
720
- strip,
721
742
  highlightsRequested: args.highlight !== undefined,
722
743
  engineHasHighlights
723
744
  });
@@ -794,14 +815,7 @@ function search(options = {}) {
794
815
  return null;
795
816
  const requested = extOptions["engine"] ?? "auto";
796
817
  const engine = resolveSearchEngine({ dialect, provider, requested });
797
- const language = decl.language ?? extOptions["language"] ?? "english";
798
- if (!isSafeSqlName({ name: language })) {
799
- throw new VibeError2({
800
- code: "VIBE_SCHEMA_INVALID",
801
- message: `search language "${language}" is not a plain text-search config name`,
802
- meta: { model: model.name, language }
803
- });
804
- }
818
+ const language = resolveLanguage({ model, decl, options: extOptions });
805
819
  const pkField = primaryKeyFieldOf({ model });
806
820
  const rank = resolveRank({ decl, options: extOptions, engine, dialect });
807
821
  assertBm25Declarations({ decl, engine, dialect });
@@ -898,4 +912,4 @@ export {
898
912
  search
899
913
  };
900
914
 
901
- //# debugId=B339B57B135FF5BD64756E2164756E21
915
+ //# debugId=8DD793DC3166C4C964756E2164756E21