@atscript/db-mongo 0.1.148 → 0.1.150

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/dist/agg.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_mongo_accumulator = require("./mongo-accumulator-BKMdRf3s.cjs");
2
+ const require_mongo_accumulator = require("./mongo-accumulator-BQBMx4GC.cjs");
3
3
  let _atscript_db = require("@atscript/db");
4
4
  let _atscript_db_agg = require("@atscript/db/agg");
5
5
  let _uniqu_core = require("@uniqu/core");
package/dist/agg.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { d as orNull, i as buildMongoFilter, l as exprToMongo, n as distinctCountExpr, s as mongoFilterStages, t as buildAccumulator, u as notNullExpr } from "./mongo-accumulator-BvJNCVoc.mjs";
1
+ import { d as orNull, i as buildMongoFilter, l as exprToMongo, n as distinctCountExpr, s as mongoFilterStages, t as buildAccumulator, u as notNullExpr } from "./mongo-accumulator-DYhg8usT.mjs";
2
2
  import { evaluateExpr } from "@atscript/db";
3
3
  import { resolveAlias } from "@atscript/db/agg";
4
4
  import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_mongo_accumulator = require("./mongo-accumulator-BKMdRf3s.cjs");
2
+ const require_mongo_accumulator = require("./mongo-accumulator-BQBMx4GC.cjs");
3
3
  let _atscript_db = require("@atscript/db");
4
4
  let mongodb = require("mongodb");
5
5
  let _atscript_db_agg = require("@atscript/db/agg");
@@ -908,10 +908,24 @@ function buildSearchStage(host, text, indexName, controls) {
908
908
  const index = host.getMongoSearchIndex(indexName);
909
909
  if (!index) return;
910
910
  if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
911
- if (index.type === "text") return {
912
- stage: { $match: { $text: { $search: text } } },
913
- classicText: true
914
- };
911
+ if (index.type === "text") {
912
+ const n = (0, _atscript_db.searchTermInteger)(text);
913
+ const keys = n === void 0 ? [] : host.getNumericSearchKeys(index);
914
+ const hasText = Object.keys(index.fields).length > 0;
915
+ const equalities = keys.map((key) => ({ [key]: {
916
+ $eq: n,
917
+ $type: "number"
918
+ } }));
919
+ if (!hasText) return {
920
+ stage: { $match: equalities.length === 0 ? { _id: { $in: [] } } : equalities.length === 1 ? equalities[0] : { $or: equalities } },
921
+ classicText: false
922
+ };
923
+ const textMatch = { $text: { $search: text } };
924
+ return {
925
+ stage: { $match: equalities.length > 0 ? { $or: [textMatch, ...equalities] } : textMatch },
926
+ classicText: true
927
+ };
928
+ }
915
929
  const searchIndex = index;
916
930
  const fuzzy = resolveSearchFuzzy(searchIndex, controls);
917
931
  const strategy = searchIndex.strategy ?? "compound";
@@ -949,6 +963,11 @@ function buildSearchStage(host, text, indexName, controls) {
949
963
  for (const path of paths.autocompleteOutside) clauses.push(autocompleteOp(path));
950
964
  for (const group of paths.arrayGroups) for (const leaf of group.autocompleteLeaves) clauses.push(embeddedAutocompleteClause(group.chain, leaf));
951
965
  }
966
+ const n = (0, _atscript_db.searchTermInteger)(text);
967
+ if (n !== void 0) for (const path of host.getNumericSearchKeys(index)) clauses.push({ equals: {
968
+ path,
969
+ value: n
970
+ } });
952
971
  let body;
953
972
  if (clauses.length <= 1) body = clauses[0] ?? textClause();
954
973
  else body = { compound: {
@@ -1517,7 +1536,9 @@ async function syncIndexesImpl(host) {
1517
1536
  let mongoType;
1518
1537
  if (index.type === "fulltext") {
1519
1538
  mongoType = "text";
1520
- for (const f of index.fields) {
1539
+ const textFields = index.fields.filter((f) => !f.integer);
1540
+ if (textFields.length === 0) continue;
1541
+ for (const f of textFields) {
1521
1542
  fields[f.name] = "text";
1522
1543
  weights[f.name] = f.weight ?? 1;
1523
1544
  }
@@ -2113,13 +2134,13 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2113
2134
  createValidator: (opts) => this._table.createValidator(opts)
2114
2135
  };
2115
2136
  }
2116
- async nativePatch(filter, patch, ops, expectedVersion) {
2137
+ async nativePatch(filter, patch, ops, expectedVersion, opts) {
2117
2138
  const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "nativePatch");
2118
2139
  if (!mongoFilter) return {
2119
2140
  matchedCount: 0,
2120
2141
  modifiedCount: 0
2121
2142
  };
2122
- const versionColumn = this._table.versionColumnPhysical;
2143
+ const versionColumn = this._versionColumnFor(opts, expectedVersion);
2123
2144
  const effectiveOps = versionColumn !== void 0 ? {
2124
2145
  ...ops,
2125
2146
  inc: {
@@ -2227,6 +2248,18 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2227
2248
  for (const field of this._pendingUniqueFields) this._addMongoIndexField("unique", "__pk", meta.documentPath(field));
2228
2249
  for (const pending of this._pendingSearchFields) this._addFieldToSearchIndex("search_text", pending.indexName, pending.field, pending.mappings);
2229
2250
  this._pendingSearchFields = [];
2251
+ const textBearing = [...this._table.indexes.values()].filter((i) => i.type === "fulltext" && i.fields.some((f) => !f.integer));
2252
+ if (textBearing.length > 1) throw new Error(`MongoDB allows one text index per collection: "${this._table.tableName}" declares ${textBearing.length} @db.index.fulltext names with text fields (${textBearing.map((i) => `"${i.name}"`).join(", ")}). Merge them into one name; integer-only fulltext indexes may be declared separately.`);
2253
+ for (const fulltext of this._table.indexes.values()) {
2254
+ if (fulltext.type !== "fulltext" || !fulltext.fields.some((f) => f.integer)) continue;
2255
+ const logical = this._indexLogicalPaths(fulltext);
2256
+ for (const index of this._mongoIndexes.values()) {
2257
+ if (index.type !== "search_text") continue;
2258
+ fulltext.fields.forEach((f, i) => {
2259
+ if (f.integer) this._addFieldToSearchIndex("search_text", index.name, logical[i], [{ type: "number" }]);
2260
+ });
2261
+ }
2262
+ }
2230
2263
  for (const [key, value] of this._vectorFilters.entries()) {
2231
2264
  const index = this._mongoIndexes.get(key);
2232
2265
  if (index && index.type === "vector") index.definition.fields?.push({
@@ -2250,13 +2283,19 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2250
2283
  this._table.flatMap;
2251
2284
  this._searchIndexesMap = /* @__PURE__ */ new Map();
2252
2285
  let defaultIndex;
2253
- for (const index of this._table.indexes.values()) if (index.type === "fulltext" && !defaultIndex) defaultIndex = {
2254
- key: index.key,
2255
- name: index.name,
2256
- type: "text",
2257
- fields: Object.fromEntries(index.fields.map((f) => [f.name, "text"])),
2258
- weights: Object.fromEntries(index.fields.filter((f) => f.weight).map((f) => [f.name, f.weight]))
2259
- };
2286
+ const fulltexts = [...this._table.indexes.values()].filter((i) => i.type === "fulltext");
2287
+ const defaultFulltext = (0, _atscript_db.defaultFulltextIndex)(fulltexts);
2288
+ for (const index of fulltexts) {
2289
+ const converted = {
2290
+ key: index.key,
2291
+ name: index.name,
2292
+ type: "text",
2293
+ fields: Object.fromEntries(index.fields.filter((f) => !f.integer).map((f) => [f.name, "text"])),
2294
+ weights: Object.fromEntries(index.fields.filter((f) => f.weight && !f.integer).map((f) => [f.name, f.weight]))
2295
+ };
2296
+ if (index === defaultFulltext) defaultIndex = converted;
2297
+ else if (!this._searchIndexesMap.has(index.name)) this._searchIndexesMap.set(index.name, converted);
2298
+ }
2260
2299
  for (const index of this._mongoIndexes.values()) switch (index.type) {
2261
2300
  case "text":
2262
2301
  if (!defaultIndex) defaultIndex = index;
@@ -2277,6 +2316,27 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2277
2316
  }
2278
2317
  return this._searchIndexesMap;
2279
2318
  }
2319
+ /**
2320
+ * Stored paths of the integer members matched by exact number next to a
2321
+ * text search on `index` (since 0.1.150). A classic text index contributes
2322
+ * the integer members of its own `@db.index.fulltext` (per index, as on the
2323
+ * SQL adapters); an Atlas index the integer members it maps (every fulltext
2324
+ * integer member is mapped, so they are in its reported `paths`).
2325
+ */
2326
+ getNumericSearchKeys(index) {
2327
+ this._table.flatMap;
2328
+ const keys = /* @__PURE__ */ new Set();
2329
+ if (index.type === "text") {
2330
+ const source = this._table.indexes.get(index.key);
2331
+ if (source?.type === "fulltext") {
2332
+ for (const f of source.fields) if (f.integer) keys.add(f.name);
2333
+ }
2334
+ } else for (const fulltext of this._table.indexes.values()) {
2335
+ if (fulltext.type !== "fulltext") continue;
2336
+ for (const f of fulltext.fields) if (f.integer) keys.add(f.name);
2337
+ }
2338
+ return [...keys];
2339
+ }
2280
2340
  /** Returns a specific MongoDB search index by name. */
2281
2341
  getMongoSearchIndex(name = DEFAULT_INDEX_NAME) {
2282
2342
  return this.getMongoSearchIndexes().get(name);
@@ -2620,13 +2680,13 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2620
2680
  ...this._getSessionOpts()
2621
2681
  });
2622
2682
  }
2623
- async updateOne(filter, data, ops, expectedVersion) {
2683
+ async updateOne(filter, data, ops, expectedVersion, opts) {
2624
2684
  const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "updateOne");
2625
2685
  if (!mongoFilter) return {
2626
2686
  matchedCount: 0,
2627
2687
  modifiedCount: 0
2628
2688
  };
2629
- const updateDoc = buildMongoUpdateDoc(data, ops, this._table.versionColumnPhysical);
2689
+ const updateDoc = buildMongoUpdateDoc(data, ops, this._versionColumnFor(opts, expectedVersion));
2630
2690
  this._log("updateOne", mongoFilter, updateDoc);
2631
2691
  return this._wrapUpdate(() => this.collection.updateOne(mongoFilter, updateDoc, this._getSessionOpts()));
2632
2692
  }
@@ -2653,9 +2713,8 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2653
2713
  this._log("deleteOne", mongoFilter);
2654
2714
  return { deletedCount: (await this.collection.deleteOne(mongoFilter, this._getSessionOpts())).deletedCount };
2655
2715
  }
2656
- async updateMany(filter, data, ops) {
2657
- const versionColumn = this._table.versionColumnPhysical;
2658
- const updateDoc = buildMongoUpdateDoc(data, ops, versionColumn);
2716
+ async updateMany(filter, data, ops, opts) {
2717
+ const updateDoc = buildMongoUpdateDoc(data, ops, this._versionColumnFor(opts));
2659
2718
  return this._updateMatching(filter, (mongoFilter) => {
2660
2719
  this._log("updateMany", mongoFilter, updateDoc);
2661
2720
  return this._wrapUpdate(() => this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts()));
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as buildMongoFilter, c as mongoFilterStages, i as TMongoFilterPlan, l as planStages, n as TMongoFieldCollation, o as buildMongoQuery, r as TMongoFilterOptions, s as collationOfAdapter, t as REL_FILTER_TEMP_PREFIX } from "./mongo-filter-CGd9ryOD.cjs";
2
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
2
+ import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateOptions, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
3
3
  import { AggregateOptions, AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
4
4
  import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
5
5
  import { AggregateFn, BucketUnit } from "@uniqu/core";
@@ -402,13 +402,21 @@ declare class MongoAdapter extends BaseDbAdapter {
402
402
  loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[], relations: ReadonlyMap<string, TDbRelation>, foreignKeys: ReadonlyMap<string, TDbForeignKey>, tableResolver?: TTableResolver): Promise<void>;
403
403
  /** Returns the context object used by CollectionPatcher. */
404
404
  getPatcherContext(): TCollectionPatcherContext;
405
- nativePatch(filter: FilterExpr, patch: unknown, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
405
+ nativePatch(filter: FilterExpr, patch: unknown, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
406
406
  onBeforeFlatten(_type: unknown): void;
407
407
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
408
408
  getMetadataOverrides(meta: TableMetadata): TMetadataOverrides;
409
409
  onAfterFlatten(): void;
410
410
  /** Returns MongoDB-specific search index map (internal). */
411
411
  getMongoSearchIndexes(): Map<string, TMongoIndex>;
412
+ /**
413
+ * Stored paths of the integer members matched by exact number next to a
414
+ * text search on `index` (since 0.1.150). A classic text index contributes
415
+ * the integer members of its own `@db.index.fulltext` (per index, as on the
416
+ * SQL adapters); an Atlas index the integer members it maps (every fulltext
417
+ * integer member is mapped, so they are in its reported `paths`).
418
+ */
419
+ getNumericSearchKeys(index: TMongoIndex): readonly string[];
412
420
  /** Returns a specific MongoDB search index by name. */
413
421
  getMongoSearchIndex(name?: string): TMongoIndex | undefined;
414
422
  /** Returns the default similarity threshold for a vector index (from @db.search.vector.threshold). */
@@ -515,10 +523,10 @@ declare class MongoAdapter extends BaseDbAdapter {
515
523
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
516
524
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
517
525
  count(query: DbQuery): Promise<number>;
518
- updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
526
+ updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
519
527
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
520
528
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
521
- updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
529
+ updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
522
530
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
523
531
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
524
532
  clearCollectionCache(): void;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as buildMongoFilter, c as mongoFilterStages, i as TMongoFilterPlan, l as planStages, n as TMongoFieldCollation, o as buildMongoQuery, r as TMongoFilterOptions, s as collationOfAdapter, t as REL_FILTER_TEMP_PREFIX } from "./mongo-filter-CGd9ryOD.mjs";
2
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
2
+ import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbCollation, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateOptions, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TViewCapability, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
3
3
  import { AggregateOptions, AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
4
4
  import { AggregateFn, BucketUnit } from "@uniqu/core";
5
5
  import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
@@ -402,13 +402,21 @@ declare class MongoAdapter extends BaseDbAdapter {
402
402
  loadRelations(rows: Array<Record<string, unknown>>, withRelations: WithRelation[], relations: ReadonlyMap<string, TDbRelation>, foreignKeys: ReadonlyMap<string, TDbForeignKey>, tableResolver?: TTableResolver): Promise<void>;
403
403
  /** Returns the context object used by CollectionPatcher. */
404
404
  getPatcherContext(): TCollectionPatcherContext;
405
- nativePatch(filter: FilterExpr, patch: unknown, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
405
+ nativePatch(filter: FilterExpr, patch: unknown, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
406
406
  onBeforeFlatten(_type: unknown): void;
407
407
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
408
408
  getMetadataOverrides(meta: TableMetadata): TMetadataOverrides;
409
409
  onAfterFlatten(): void;
410
410
  /** Returns MongoDB-specific search index map (internal). */
411
411
  getMongoSearchIndexes(): Map<string, TMongoIndex>;
412
+ /**
413
+ * Stored paths of the integer members matched by exact number next to a
414
+ * text search on `index` (since 0.1.150). A classic text index contributes
415
+ * the integer members of its own `@db.index.fulltext` (per index, as on the
416
+ * SQL adapters); an Atlas index the integer members it maps (every fulltext
417
+ * integer member is mapped, so they are in its reported `paths`).
418
+ */
419
+ getNumericSearchKeys(index: TMongoIndex): readonly string[];
412
420
  /** Returns a specific MongoDB search index by name. */
413
421
  getMongoSearchIndex(name?: string): TMongoIndex | undefined;
414
422
  /** Returns the default similarity threshold for a vector index (from @db.search.vector.threshold). */
@@ -515,10 +523,10 @@ declare class MongoAdapter extends BaseDbAdapter {
515
523
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
516
524
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
517
525
  count(query: DbQuery): Promise<number>;
518
- updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
526
+ updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
519
527
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
520
528
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
521
- updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
529
+ updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
522
530
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
523
531
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
524
532
  clearCollectionCache(): void;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { a as buildMongoQuery, c as planStages, d as orNull, f as queryNodeToExpr, i as buildMongoFilter, l as exprToMongo, n as distinctCountExpr, o as collationOfAdapter, p as correlate, r as REL_FILTER_TEMP_PREFIX, s as mongoFilterStages, t as buildAccumulator } from "./mongo-accumulator-BvJNCVoc.mjs";
2
- import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, andFilters, bucketTimeZoneUnavailable, computeInsights, containsRelationFilter, createFailureCollector, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, getPath, isAtscriptDbView, isFieldRef, relationStaticFilter, resolveDesignType, searchIndexNotFoundMessage, tableNameOf, translateQueryTree, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
1
+ import { a as buildMongoQuery, c as planStages, d as orNull, f as queryNodeToExpr, i as buildMongoFilter, l as exprToMongo, n as distinctCountExpr, o as collationOfAdapter, p as correlate, r as REL_FILTER_TEMP_PREFIX, s as mongoFilterStages, t as buildAccumulator } from "./mongo-accumulator-DYhg8usT.mjs";
2
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, andFilters, bucketTimeZoneUnavailable, computeInsights, containsRelationFilter, createFailureCollector, defaultFulltextIndex, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, getPath, isAtscriptDbView, isFieldRef, relationStaticFilter, resolveDesignType, searchIndexNotFoundMessage, searchTermInteger, tableNameOf, translateQueryTree, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
3
3
  import { MongoBulkWriteError, MongoClient, MongoServerError, ObjectId } from "mongodb";
4
4
  import { resolveAggregateSearch } from "@atscript/db/agg";
5
5
  //#region src/lib/path-utils.ts
@@ -907,10 +907,24 @@ function buildSearchStage(host, text, indexName, controls) {
907
907
  const index = host.getMongoSearchIndex(indexName);
908
908
  if (!index) return;
909
909
  if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
910
- if (index.type === "text") return {
911
- stage: { $match: { $text: { $search: text } } },
912
- classicText: true
913
- };
910
+ if (index.type === "text") {
911
+ const n = searchTermInteger(text);
912
+ const keys = n === void 0 ? [] : host.getNumericSearchKeys(index);
913
+ const hasText = Object.keys(index.fields).length > 0;
914
+ const equalities = keys.map((key) => ({ [key]: {
915
+ $eq: n,
916
+ $type: "number"
917
+ } }));
918
+ if (!hasText) return {
919
+ stage: { $match: equalities.length === 0 ? { _id: { $in: [] } } : equalities.length === 1 ? equalities[0] : { $or: equalities } },
920
+ classicText: false
921
+ };
922
+ const textMatch = { $text: { $search: text } };
923
+ return {
924
+ stage: { $match: equalities.length > 0 ? { $or: [textMatch, ...equalities] } : textMatch },
925
+ classicText: true
926
+ };
927
+ }
914
928
  const searchIndex = index;
915
929
  const fuzzy = resolveSearchFuzzy(searchIndex, controls);
916
930
  const strategy = searchIndex.strategy ?? "compound";
@@ -948,6 +962,11 @@ function buildSearchStage(host, text, indexName, controls) {
948
962
  for (const path of paths.autocompleteOutside) clauses.push(autocompleteOp(path));
949
963
  for (const group of paths.arrayGroups) for (const leaf of group.autocompleteLeaves) clauses.push(embeddedAutocompleteClause(group.chain, leaf));
950
964
  }
965
+ const n = searchTermInteger(text);
966
+ if (n !== void 0) for (const path of host.getNumericSearchKeys(index)) clauses.push({ equals: {
967
+ path,
968
+ value: n
969
+ } });
951
970
  let body;
952
971
  if (clauses.length <= 1) body = clauses[0] ?? textClause();
953
972
  else body = { compound: {
@@ -1516,7 +1535,9 @@ async function syncIndexesImpl(host) {
1516
1535
  let mongoType;
1517
1536
  if (index.type === "fulltext") {
1518
1537
  mongoType = "text";
1519
- for (const f of index.fields) {
1538
+ const textFields = index.fields.filter((f) => !f.integer);
1539
+ if (textFields.length === 0) continue;
1540
+ for (const f of textFields) {
1520
1541
  fields[f.name] = "text";
1521
1542
  weights[f.name] = f.weight ?? 1;
1522
1543
  }
@@ -2112,13 +2133,13 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2112
2133
  createValidator: (opts) => this._table.createValidator(opts)
2113
2134
  };
2114
2135
  }
2115
- async nativePatch(filter, patch, ops, expectedVersion) {
2136
+ async nativePatch(filter, patch, ops, expectedVersion, opts) {
2116
2137
  const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "nativePatch");
2117
2138
  if (!mongoFilter) return {
2118
2139
  matchedCount: 0,
2119
2140
  modifiedCount: 0
2120
2141
  };
2121
- const versionColumn = this._table.versionColumnPhysical;
2142
+ const versionColumn = this._versionColumnFor(opts, expectedVersion);
2122
2143
  const effectiveOps = versionColumn !== void 0 ? {
2123
2144
  ...ops,
2124
2145
  inc: {
@@ -2226,6 +2247,18 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2226
2247
  for (const field of this._pendingUniqueFields) this._addMongoIndexField("unique", "__pk", meta.documentPath(field));
2227
2248
  for (const pending of this._pendingSearchFields) this._addFieldToSearchIndex("search_text", pending.indexName, pending.field, pending.mappings);
2228
2249
  this._pendingSearchFields = [];
2250
+ const textBearing = [...this._table.indexes.values()].filter((i) => i.type === "fulltext" && i.fields.some((f) => !f.integer));
2251
+ if (textBearing.length > 1) throw new Error(`MongoDB allows one text index per collection: "${this._table.tableName}" declares ${textBearing.length} @db.index.fulltext names with text fields (${textBearing.map((i) => `"${i.name}"`).join(", ")}). Merge them into one name; integer-only fulltext indexes may be declared separately.`);
2252
+ for (const fulltext of this._table.indexes.values()) {
2253
+ if (fulltext.type !== "fulltext" || !fulltext.fields.some((f) => f.integer)) continue;
2254
+ const logical = this._indexLogicalPaths(fulltext);
2255
+ for (const index of this._mongoIndexes.values()) {
2256
+ if (index.type !== "search_text") continue;
2257
+ fulltext.fields.forEach((f, i) => {
2258
+ if (f.integer) this._addFieldToSearchIndex("search_text", index.name, logical[i], [{ type: "number" }]);
2259
+ });
2260
+ }
2261
+ }
2229
2262
  for (const [key, value] of this._vectorFilters.entries()) {
2230
2263
  const index = this._mongoIndexes.get(key);
2231
2264
  if (index && index.type === "vector") index.definition.fields?.push({
@@ -2249,13 +2282,19 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2249
2282
  this._table.flatMap;
2250
2283
  this._searchIndexesMap = /* @__PURE__ */ new Map();
2251
2284
  let defaultIndex;
2252
- for (const index of this._table.indexes.values()) if (index.type === "fulltext" && !defaultIndex) defaultIndex = {
2253
- key: index.key,
2254
- name: index.name,
2255
- type: "text",
2256
- fields: Object.fromEntries(index.fields.map((f) => [f.name, "text"])),
2257
- weights: Object.fromEntries(index.fields.filter((f) => f.weight).map((f) => [f.name, f.weight]))
2258
- };
2285
+ const fulltexts = [...this._table.indexes.values()].filter((i) => i.type === "fulltext");
2286
+ const defaultFulltext = defaultFulltextIndex(fulltexts);
2287
+ for (const index of fulltexts) {
2288
+ const converted = {
2289
+ key: index.key,
2290
+ name: index.name,
2291
+ type: "text",
2292
+ fields: Object.fromEntries(index.fields.filter((f) => !f.integer).map((f) => [f.name, "text"])),
2293
+ weights: Object.fromEntries(index.fields.filter((f) => f.weight && !f.integer).map((f) => [f.name, f.weight]))
2294
+ };
2295
+ if (index === defaultFulltext) defaultIndex = converted;
2296
+ else if (!this._searchIndexesMap.has(index.name)) this._searchIndexesMap.set(index.name, converted);
2297
+ }
2259
2298
  for (const index of this._mongoIndexes.values()) switch (index.type) {
2260
2299
  case "text":
2261
2300
  if (!defaultIndex) defaultIndex = index;
@@ -2276,6 +2315,27 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2276
2315
  }
2277
2316
  return this._searchIndexesMap;
2278
2317
  }
2318
+ /**
2319
+ * Stored paths of the integer members matched by exact number next to a
2320
+ * text search on `index` (since 0.1.150). A classic text index contributes
2321
+ * the integer members of its own `@db.index.fulltext` (per index, as on the
2322
+ * SQL adapters); an Atlas index the integer members it maps (every fulltext
2323
+ * integer member is mapped, so they are in its reported `paths`).
2324
+ */
2325
+ getNumericSearchKeys(index) {
2326
+ this._table.flatMap;
2327
+ const keys = /* @__PURE__ */ new Set();
2328
+ if (index.type === "text") {
2329
+ const source = this._table.indexes.get(index.key);
2330
+ if (source?.type === "fulltext") {
2331
+ for (const f of source.fields) if (f.integer) keys.add(f.name);
2332
+ }
2333
+ } else for (const fulltext of this._table.indexes.values()) {
2334
+ if (fulltext.type !== "fulltext") continue;
2335
+ for (const f of fulltext.fields) if (f.integer) keys.add(f.name);
2336
+ }
2337
+ return [...keys];
2338
+ }
2279
2339
  /** Returns a specific MongoDB search index by name. */
2280
2340
  getMongoSearchIndex(name = DEFAULT_INDEX_NAME) {
2281
2341
  return this.getMongoSearchIndexes().get(name);
@@ -2619,13 +2679,13 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2619
2679
  ...this._getSessionOpts()
2620
2680
  });
2621
2681
  }
2622
- async updateOne(filter, data, ops, expectedVersion) {
2682
+ async updateOne(filter, data, ops, expectedVersion, opts) {
2623
2683
  const mongoFilter = await this._buildCasFilter(filter, expectedVersion, "updateOne");
2624
2684
  if (!mongoFilter) return {
2625
2685
  matchedCount: 0,
2626
2686
  modifiedCount: 0
2627
2687
  };
2628
- const updateDoc = buildMongoUpdateDoc(data, ops, this._table.versionColumnPhysical);
2688
+ const updateDoc = buildMongoUpdateDoc(data, ops, this._versionColumnFor(opts, expectedVersion));
2629
2689
  this._log("updateOne", mongoFilter, updateDoc);
2630
2690
  return this._wrapUpdate(() => this.collection.updateOne(mongoFilter, updateDoc, this._getSessionOpts()));
2631
2691
  }
@@ -2652,9 +2712,8 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2652
2712
  this._log("deleteOne", mongoFilter);
2653
2713
  return { deletedCount: (await this.collection.deleteOne(mongoFilter, this._getSessionOpts())).deletedCount };
2654
2714
  }
2655
- async updateMany(filter, data, ops) {
2656
- const versionColumn = this._table.versionColumnPhysical;
2657
- const updateDoc = buildMongoUpdateDoc(data, ops, versionColumn);
2715
+ async updateMany(filter, data, ops, opts) {
2716
+ const updateDoc = buildMongoUpdateDoc(data, ops, this._versionColumnFor(opts));
2658
2717
  return this._updateMatching(filter, (mongoFilter) => {
2659
2718
  this._log("updateMany", mongoFilter, updateDoc);
2660
2719
  return this._wrapUpdate(() => this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts()));
@@ -195,6 +195,20 @@ const mongoVisitor = {
195
195
  $options: flags
196
196
  } } : { [field]: { $regex: pattern } };
197
197
  }
198
+ if (op === _atscript_db.INTEGER_REGEX_OP) {
199
+ const { pattern, flags } = parseRegexString(value);
200
+ const options = flags.replace(/[^imsx]/g, "");
201
+ return { $expr: { $regexMatch: {
202
+ input: { $toString: { $convert: {
203
+ input: `$${field}`,
204
+ to: "long",
205
+ onError: null,
206
+ onNull: null
207
+ } } },
208
+ regex: pattern,
209
+ ...options ? { options } : {}
210
+ } } };
211
+ }
198
212
  if (op === "$geoWithin") {
199
213
  const { center, radius } = value;
200
214
  return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
@@ -262,6 +276,7 @@ function exactNocase(value) {
262
276
  /** Operators whose result never depends on a string collation. */
263
277
  const COLLATION_FREE_OPS = new Set([
264
278
  "$regex",
279
+ _atscript_db.INTEGER_REGEX_OP,
265
280
  "$exists",
266
281
  "$geoWithin"
267
282
  ]);
@@ -1,4 +1,4 @@
1
- import { DbError, andFilters, containsRelationFilter, isFieldRef, isResolvedRelationFilter, walkFilter } from "@atscript/db";
1
+ import { DbError, INTEGER_REGEX_OP, andFilters, containsRelationFilter, isFieldRef, isResolvedRelationFilter, walkFilter } from "@atscript/db";
2
2
  import { BSONRegExp } from "mongodb";
3
3
  import { assertAggregateFn } from "@atscript/db/agg";
4
4
  //#region src/lib/lookup-join.ts
@@ -195,6 +195,20 @@ const mongoVisitor = {
195
195
  $options: flags
196
196
  } } : { [field]: { $regex: pattern } };
197
197
  }
198
+ if (op === INTEGER_REGEX_OP) {
199
+ const { pattern, flags } = parseRegexString(value);
200
+ const options = flags.replace(/[^imsx]/g, "");
201
+ return { $expr: { $regexMatch: {
202
+ input: { $toString: { $convert: {
203
+ input: `$${field}`,
204
+ to: "long",
205
+ onError: null,
206
+ onNull: null
207
+ } } },
208
+ regex: pattern,
209
+ ...options ? { options } : {}
210
+ } } };
211
+ }
198
212
  if (op === "$geoWithin") {
199
213
  const { center, radius } = value;
200
214
  return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
@@ -262,6 +276,7 @@ function exactNocase(value) {
262
276
  /** Operators whose result never depends on a string collation. */
263
277
  const COLLATION_FREE_OPS = new Set([
264
278
  "$regex",
279
+ INTEGER_REGEX_OP,
265
280
  "$exists",
266
281
  "$geoWithin"
267
282
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.148",
3
+ "version": "0.1.150",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -46,19 +46,19 @@
46
46
  "access": "public"
47
47
  },
48
48
  "devDependencies": {
49
- "@atscript/core": "^0.1.100",
50
- "@atscript/typescript": "^0.1.100",
49
+ "@atscript/core": "^0.1.101",
50
+ "@atscript/typescript": "^0.1.101",
51
51
  "@uniqu/core": "^0.1.13",
52
52
  "mongodb": "^6.17.0",
53
53
  "mongodb-memory-server-core": "^10.0.0",
54
- "unplugin-atscript": "^0.1.100"
54
+ "unplugin-atscript": "^0.1.101"
55
55
  },
56
56
  "peerDependencies": {
57
- "@atscript/core": "^0.1.100",
58
- "@atscript/typescript": "^0.1.100",
57
+ "@atscript/core": "^0.1.101",
58
+ "@atscript/typescript": "^0.1.101",
59
59
  "@uniqu/core": "^0.1.13",
60
60
  "mongodb": "^6.17.0",
61
- "@atscript/db": "^0.1.148"
61
+ "@atscript/db": "^0.1.150"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",