@vibeorm/extensions 2.3.2 → 3.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VibeORM contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -73,6 +73,8 @@ const hits = await db.post.search({
73
73
 
74
74
  `highlights` is `readonly string[] | null`; it is `null` when no highlight was requested and on engines without highlighting.
75
75
 
76
+ `row` holds the model's columns, read exactly as `findMany` reads them, for `search` and for vector `nearest`. Generated artifact columns (the tsvector column) and `Unsupported(...)` columns such as the vector itself are not model fields and are not returned.
77
+
76
78
  ### Engines per dialect
77
79
 
78
80
  `engine: "auto"` (the default) picks the dialect's native engine. `resolveSearchEngine({ dialect, provider, requested })` is the exported resolution rule.
package/dist/index.js CHANGED
@@ -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
  });
@@ -632,7 +650,7 @@ function createSearchDelegate(config) {
632
650
  meta: { table: config.table }
633
651
  });
634
652
  }
635
- const projection = idOnly ? `${t}.${quote(config.pkColumn ?? "")}` : `${t}.*`;
653
+ const projection = idOnly ? `${t}.${quote(config.pkColumn ?? "")}` : ctx.projection({ qualify: "t" });
636
654
  const collector = new ParamCollector(dialect);
637
655
  const userWhere = (qualify) => {
638
656
  if (args.where === undefined)
@@ -641,13 +659,13 @@ function createSearchDelegate(config) {
641
659
  return expr === undefined ? null : renderExpr({ dialect, expr, collector });
642
660
  };
643
661
  let text;
644
- let strip = [];
645
662
  let engineHasHighlights = true;
646
663
  if (config.engine === "tsvector") {
647
664
  const language = config.language ?? "english";
648
665
  const ranker = config.rank ?? "ts_rank_cd";
649
666
  const tsv = `${t}.${quote(config.tsvColumn ?? "")}`;
650
- const tsquery = () => prefix ? `to_tsquery('${language}', ${collector.add(matchText)})` : `websearch_to_tsquery('${language}', ${collector.add(matchText)})`;
667
+ const queryParameter = dialect.name === "postgres" ? collector.add(matchText) : undefined;
668
+ const tsquery = () => prefix ? `to_tsquery('${language}', ${queryParameter ?? collector.add(matchText)})` : `websearch_to_tsquery('${language}', ${queryParameter ?? collector.add(matchText)})`;
651
669
  text = `SELECT ${projection}, ${ranker}(${tsv}, ${tsquery()}) AS ${quote(SCORE_ALIAS)}`;
652
670
  if (hlColumn !== null) {
653
671
  text += `, ts_headline('${language}', ${t}.${quote(hlColumn)}, ${tsquery()}) AS ${quote(HIGHLIGHT_ALIAS)}`;
@@ -660,7 +678,6 @@ function createSearchDelegate(config) {
660
678
  text += ` AND ${ranker}(${tsv}, ${tsquery()}) >= ${collector.add(args.minScore)}`;
661
679
  }
662
680
  text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
663
- strip = idOnly ? [] : [config.tsvColumn ?? ""];
664
681
  } else if (config.engine === "fts5") {
665
682
  const fts = quote(config.ftsTable ?? "");
666
683
  text = `SELECT ${projection}, -bm25(${fts}) AS ${quote(SCORE_ALIAS)}`;
@@ -717,7 +734,6 @@ function createSearchDelegate(config) {
717
734
  const rows = await ctx.executeRows({ text, values: collector.values, method: "search" });
718
735
  return shapeHits({
719
736
  rows,
720
- strip,
721
737
  highlightsRequested: args.highlight !== undefined,
722
738
  engineHasHighlights
723
739
  });
@@ -794,14 +810,7 @@ function search(options = {}) {
794
810
  return null;
795
811
  const requested = extOptions["engine"] ?? "auto";
796
812
  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
- }
813
+ const language = resolveLanguage({ model, decl, options: extOptions });
805
814
  const pkField = primaryKeyFieldOf({ model });
806
815
  const rank = resolveRank({ decl, options: extOptions, engine, dialect });
807
816
  assertBm25Declarations({ decl, engine, dialect });
@@ -1055,7 +1064,8 @@ function createVectorNamespace(config) {
1055
1064
  const op = METRIC_OPERATOR[metric];
1056
1065
  const col = `${t}.${quote(column)}`;
1057
1066
  const collector = new ParamCollector2(dialect);
1058
- let text = `SELECT ${t}.*, (${col} ${op} ${collector.add(literal)}) AS ${quote(DISTANCE_ALIAS)}`;
1067
+ const vector = dialect.name === "postgres" ? collector.add(literal) : undefined;
1068
+ let text = `SELECT ${ctx.projection({ qualify: "t" })}, (${col} ${op} ${vector ?? collector.add(literal)}) AS ${quote(DISTANCE_ALIAS)}`;
1059
1069
  text += ` FROM ${quote(config.table)} AS ${t}`;
1060
1070
  text += ` WHERE ${col} IS NOT NULL`;
1061
1071
  if (args.where !== undefined) {
@@ -1064,9 +1074,9 @@ function createVectorNamespace(config) {
1064
1074
  text += ` AND (${renderExpr2({ dialect, expr, collector })})`;
1065
1075
  }
1066
1076
  if (args.maxDistance !== undefined) {
1067
- text += ` AND (${col} ${op} ${collector.add(literal)}) < ${collector.add(args.maxDistance)}`;
1077
+ text += ` AND (${col} ${op} ${vector ?? collector.add(literal)}) < ${collector.add(args.maxDistance)}`;
1068
1078
  }
1069
- text += ` ORDER BY ${col} ${op} ${collector.add(literal)} LIMIT ${take}`;
1079
+ text += ` ORDER BY ${col} ${op} ${vector ?? collector.add(literal)} LIMIT ${take}`;
1070
1080
  const rows = await ctx.executeRows({
1071
1081
  text,
1072
1082
  values: collector.values,
@@ -1400,4 +1410,4 @@ export {
1400
1410
  vector
1401
1411
  };
1402
1412
 
1403
- //# debugId=8FC9F7443F3A892664756E2164756E21
1413
+ //# debugId=6C7F38EC9C7882FB64756E2164756E21