joist-core 2.3.0-next.99 → 2.4.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.
Files changed (131) hide show
  1. package/build/EntityManager.cjs +1 -1
  2. package/build/EntityManager.cjs.map +1 -1
  3. package/build/EntityManager.d.cts +9 -8
  4. package/build/EntityManager.d.cts.map +1 -1
  5. package/build/EntityManager.d.mts +9 -8
  6. package/build/EntityManager.d.mts.map +1 -1
  7. package/build/EntityManager.js +1 -1
  8. package/build/EntityManager.js.map +1 -1
  9. package/build/dataloaders/findCountDataLoader.cjs +2 -0
  10. package/build/dataloaders/findCountDataLoader.cjs.map +1 -1
  11. package/build/dataloaders/findCountDataLoader.d.cts.map +1 -1
  12. package/build/dataloaders/findCountDataLoader.d.mts.map +1 -1
  13. package/build/dataloaders/findCountDataLoader.js +2 -0
  14. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  15. package/build/dataloaders/findDataLoader.cjs +1 -0
  16. package/build/dataloaders/findDataLoader.cjs.map +1 -1
  17. package/build/dataloaders/findDataLoader.d.cts.map +1 -1
  18. package/build/dataloaders/findDataLoader.d.mts.map +1 -1
  19. package/build/dataloaders/findDataLoader.js +2 -1
  20. package/build/dataloaders/findDataLoader.js.map +1 -1
  21. package/build/dataloaders/findIdsDataLoader.cjs +2 -0
  22. package/build/dataloaders/findIdsDataLoader.cjs.map +1 -1
  23. package/build/dataloaders/findIdsDataLoader.d.cts.map +1 -1
  24. package/build/dataloaders/findIdsDataLoader.d.mts.map +1 -1
  25. package/build/dataloaders/findIdsDataLoader.js +2 -0
  26. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  27. package/build/dataloaders/findPaginatedDataLoader.cjs +2 -0
  28. package/build/dataloaders/findPaginatedDataLoader.cjs.map +1 -1
  29. package/build/dataloaders/findPaginatedDataLoader.d.cts.map +1 -1
  30. package/build/dataloaders/findPaginatedDataLoader.d.mts.map +1 -1
  31. package/build/dataloaders/findPaginatedDataLoader.js +2 -0
  32. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -1
  33. package/build/index.cjs +6 -3
  34. package/build/index.cjs.map +1 -1
  35. package/build/index.d.cts +6 -4
  36. package/build/index.d.cts.map +1 -1
  37. package/build/index.d.mts +6 -4
  38. package/build/index.d.mts.map +1 -1
  39. package/build/index.js +6 -5
  40. package/build/index.js.map +1 -1
  41. package/build/keys.cjs +6 -1
  42. package/build/keys.cjs.map +1 -1
  43. package/build/keys.d.cts +3 -1
  44. package/build/keys.d.cts.map +1 -1
  45. package/build/keys.d.mts +3 -1
  46. package/build/keys.d.mts.map +1 -1
  47. package/build/keys.js +6 -2
  48. package/build/keys.js.map +1 -1
  49. package/build/queries/find/QueryParser.cjs +1 -2
  50. package/build/queries/find/QueryParser.cjs.map +1 -1
  51. package/build/queries/find/QueryParser.d.cts.map +1 -1
  52. package/build/queries/find/QueryParser.d.mts.map +1 -1
  53. package/build/queries/find/QueryParser.js +2 -3
  54. package/build/queries/find/QueryParser.js.map +1 -1
  55. package/build/queries/find/QueryVisitor.cjs +17 -0
  56. package/build/queries/find/QueryVisitor.cjs.map +1 -1
  57. package/build/queries/find/QueryVisitor.d.cts +3 -1
  58. package/build/queries/find/QueryVisitor.d.cts.map +1 -1
  59. package/build/queries/find/QueryVisitor.d.mts +3 -1
  60. package/build/queries/find/QueryVisitor.d.mts.map +1 -1
  61. package/build/queries/find/QueryVisitor.js +17 -1
  62. package/build/queries/find/QueryVisitor.js.map +1 -1
  63. package/build/queries/sql/Expr.cjs +18 -1
  64. package/build/queries/sql/Expr.cjs.map +1 -1
  65. package/build/queries/sql/Expr.d.cts +13 -3
  66. package/build/queries/sql/Expr.d.cts.map +1 -1
  67. package/build/queries/sql/Expr.d.mts +13 -3
  68. package/build/queries/sql/Expr.d.mts.map +1 -1
  69. package/build/queries/sql/Expr.js +18 -2
  70. package/build/queries/sql/Expr.js.map +1 -1
  71. package/build/queries/sql/JoinTree.cjs +72 -0
  72. package/build/queries/sql/JoinTree.cjs.map +1 -0
  73. package/build/queries/sql/JoinTree.d.cts +107 -0
  74. package/build/queries/sql/JoinTree.d.cts.map +1 -0
  75. package/build/queries/sql/JoinTree.d.mts +107 -0
  76. package/build/queries/sql/JoinTree.d.mts.map +1 -0
  77. package/build/queries/sql/JoinTree.js +71 -0
  78. package/build/queries/sql/JoinTree.js.map +1 -0
  79. package/build/queries/sql/Tables.cjs +103 -6
  80. package/build/queries/sql/Tables.cjs.map +1 -1
  81. package/build/queries/sql/Tables.d.cts +72 -13
  82. package/build/queries/sql/Tables.d.cts.map +1 -1
  83. package/build/queries/sql/Tables.d.mts +72 -13
  84. package/build/queries/sql/Tables.d.mts.map +1 -1
  85. package/build/queries/sql/Tables.js +101 -10
  86. package/build/queries/sql/Tables.js.map +1 -1
  87. package/build/queries/sql/custom.cjs +52 -0
  88. package/build/queries/sql/custom.cjs.map +1 -0
  89. package/build/queries/sql/custom.d.cts +85 -0
  90. package/build/queries/sql/custom.d.cts.map +1 -0
  91. package/build/queries/sql/custom.d.mts +85 -0
  92. package/build/queries/sql/custom.d.mts.map +1 -0
  93. package/build/queries/sql/custom.js +49 -0
  94. package/build/queries/sql/custom.js.map +1 -0
  95. package/build/queries/sql/execute.cjs +68 -28
  96. package/build/queries/sql/execute.cjs.map +1 -1
  97. package/build/queries/sql/execute.d.cts +110 -11
  98. package/build/queries/sql/execute.d.cts.map +1 -1
  99. package/build/queries/sql/execute.d.mts +110 -11
  100. package/build/queries/sql/execute.d.mts.map +1 -1
  101. package/build/queries/sql/execute.js +69 -29
  102. package/build/queries/sql/execute.js.map +1 -1
  103. package/build/queries/sql/expressions/types.d.cts +5 -5
  104. package/build/queries/sql/expressions/types.d.cts.map +1 -1
  105. package/build/queries/sql/expressions/types.d.mts +5 -5
  106. package/build/queries/sql/expressions/types.d.mts.map +1 -1
  107. package/build/queries/sql/keywords.cjs +1 -1
  108. package/build/queries/sql/keywords.cjs.map +1 -1
  109. package/build/queries/sql/keywords.d.cts +1 -1
  110. package/build/queries/sql/keywords.d.mts +1 -1
  111. package/build/queries/sql/keywords.js +1 -1
  112. package/build/queries/sql/keywords.js.map +1 -1
  113. package/build/queries/sql/query.cjs +100 -38
  114. package/build/queries/sql/query.cjs.map +1 -1
  115. package/build/queries/sql/query.d.cts +96 -46
  116. package/build/queries/sql/query.d.cts.map +1 -1
  117. package/build/queries/sql/query.d.mts +96 -46
  118. package/build/queries/sql/query.d.mts.map +1 -1
  119. package/build/queries/sql/query.js +102 -40
  120. package/build/queries/sql/query.js.map +1 -1
  121. package/build/queries/valueFilters.cjs +28 -36
  122. package/build/queries/valueFilters.cjs.map +1 -1
  123. package/build/queries/valueFilters.d.cts +2 -4
  124. package/build/queries/valueFilters.d.cts.map +1 -1
  125. package/build/queries/valueFilters.d.mts +2 -4
  126. package/build/queries/valueFilters.d.mts.map +1 -1
  127. package/build/queries/valueFilters.js +29 -36
  128. package/build/queries/valueFilters.js.map +1 -1
  129. package/build/serde/serde.cjs.map +1 -1
  130. package/build/serde/serde.js.map +1 -1
  131. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_queries_sql_keywords = require("../queries/sql/keywords.cjs");
3
3
  const require_queries_find_scopes = require("../queries/find/scopes.cjs");
4
+ const require_queries_find_QueryVisitor = require("../queries/find/QueryVisitor.cjs");
4
5
  const require_queries_find_QueryParser = require("../queries/find/QueryParser.cjs");
5
6
  const require_utils = require("../utils.cjs");
6
7
  const require_EntityMetadata = require("../EntityMetadata.cjs");
@@ -19,6 +20,7 @@ function findCountDataLoader(em, type, filter) {
19
20
  ...opts,
20
21
  checkLimit: false
21
22
  });
23
+ if (require_queries_find_QueryVisitor.isQueryProvablyEmpty(query)) return Promise.resolve(0);
22
24
  const bindings = [];
23
25
  require_dataloaders_findDataLoader.collectValues(bindings, query);
24
26
  const prepared = {
@@ -1 +1 @@
1
- {"version":3,"file":"findCountDataLoader.cjs","names":["getMetadata","parseFindQuery","getBatchKeyFromGenericStructure","fail","kq","collectAndReplaceArgs","buildUnnestCte","createColumnValuesFromPrepared","queryFilterHash","isScope","resolveScope","isSelectAllFilter","getEmInternalApi"],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"sourcesContent":["import {\n collectAndReplaceArgs,\n collectValues,\n createColumnValuesFromPrepared,\n getBatchKeyFromGenericStructure,\n queryFilterHash,\n} from \"src/dataloaders/findDataLoader.ts\";\nimport type { Entity, IdType } from \"src/Entity.ts\";\nimport {\n type EntityManager,\n type FindCountFilterOptions,\n type MaybeAbstractEntityConstructor,\n getEmInternalApi,\n} from \"src/EntityManager.ts\";\nimport { getMetadata } from \"src/EntityMetadata.ts\";\nimport type { FilterAndSettings, FindFilter } from \"src/queries/find/EntityFilter.ts\";\nimport type { GraphQLFilterWithAlias } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport { type ParsedFindQuery, parseFindQuery } from \"src/queries/find/QueryParser.ts\";\nimport { isScope, isSelectAllFilter, resolveScope } from \"src/queries/find/scopes.ts\";\nimport { kq } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { fail } from \"src/utils.ts\";\n\nexport const findCountOperation = \"find-count\";\n\nexport function findCountDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n): Promise<number> {\n const { where, ...opts } = filter;\n if (opts.limit || opts.offset) {\n throw new Error(\"Cannot use limit/offset with findCountDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const pendingDeletedIds = appendPendingDeletedIds(em, type, query, meta.idDbType, where, opts);\n const { findSettings } = em[\"prepareFind\"](meta, findCountOperation, query, { ...opts, checkLimit: false });\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, pendingDeletedIds, query, bindings, findSettings };\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<typeof prepared, number>(\n findCountOperation,\n batchKey,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings } = entries[0];\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query, findSettings, false);\n return [Number(rows[0].count)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT _find.tag, _data.count\n // FROM _find\n // CROSS JOIN LATERAL (\n // SELECT count(*) as count\n // FROM author a WHERE a.first_name = _find.arg1 OR a.last_name = _find.arg2\n // ) _data\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings } = entries[0];\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n\n // We're not returning the entities, just counting them...\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n\n const query2: ParsedFindQuery = {\n selects: [\"_find.tag as tag\", \"_data.count as count\"],\n tables: [\n { join: \"primary\", table: \"_find\", alias: \"_find\" },\n // Not sure what fromAlias is for/that it matters...\n { join: \"lateral\", query: query, table: meta.tableName, alias: \"_data\", fromAlias: \"_f\" },\n ],\n // For each unique query, capture its filter values in `bindings` to populate the CTE _find table\n ctes: [buildUnnestCte(\"_find\", argsColumns, createColumnValuesFromPrepared(args, entries))],\n orderBys: [],\n };\n\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query2, findSettings, false);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => 0);\n // Then put each row into the tagged query it matched\n for (const row of rows) {\n results[row.tag] = Number(row.count);\n }\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: (entry) => queryFilterHash(entry) },\n )\n .load(prepared);\n}\n\n/** Merges root-scope count/id settings with caller options, dropping pagination-only settings. */\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> } {\n if (!isScope<T>(where)) return { where, options };\n\n const resolved = resolveScope(where);\n const scopeOptions: FindCountFilterOptions<T> = {};\n if (resolved.softDeletes !== undefined) scopeOptions.softDeletes = resolved.softDeletes;\n return { where, options: { ...scopeOptions, ...options } };\n}\n\n/** Adds `id not in pendingDeletedIds` to the count query so pending deletes are excluded in SQL. */\nfunction appendPendingDeletedIds<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n query: ParsedFindQuery,\n idDbType: \"bigint\" | \"int\" | \"uuid\" | \"text\",\n where: FilterAndSettings<T>[\"where\"],\n opts: Omit<FilterAndSettings<T>, \"where\">,\n): readonly IdType[] {\n const pendingDeletedIds = isSelectAllFilter(where, opts.conditions)\n ? []\n : getEmInternalApi(em).pendingDeleteIds(type);\n if (pendingDeletedIds.length === 0) return pendingDeletedIds;\n\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n const condition = {\n kind: \"column\" as const,\n alias: primary.alias,\n column: \"id\",\n dbType: idDbType,\n cond: { kind: \"nin\" as const, value: pendingDeletedIds },\n pruneable: true,\n };\n\n if (!query.condition) {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [condition] };\n } else if (query.condition.op === \"and\") {\n query.condition.conditions.push(condition);\n } else {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [query.condition, condition] };\n }\n return pendingDeletedIds;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"findCountDataLoader.cjs","names":["getMetadata","parseFindQuery","isQueryProvablyEmpty","getBatchKeyFromGenericStructure","fail","kq","collectAndReplaceArgs","buildUnnestCte","createColumnValuesFromPrepared","queryFilterHash","isScope","resolveScope","isSelectAllFilter","getEmInternalApi"],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"sourcesContent":["import {\n collectAndReplaceArgs,\n collectValues,\n createColumnValuesFromPrepared,\n getBatchKeyFromGenericStructure,\n queryFilterHash,\n} from \"src/dataloaders/findDataLoader.ts\";\nimport type { Entity, IdType } from \"src/Entity.ts\";\nimport {\n type EntityManager,\n type FindCountFilterOptions,\n type MaybeAbstractEntityConstructor,\n getEmInternalApi,\n} from \"src/EntityManager.ts\";\nimport { getMetadata } from \"src/EntityMetadata.ts\";\nimport type { FilterAndSettings, FindFilter } from \"src/queries/find/EntityFilter.ts\";\nimport type { GraphQLFilterWithAlias } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport { type ParsedFindQuery, parseFindQuery } from \"src/queries/find/QueryParser.ts\";\nimport { isQueryProvablyEmpty } from \"src/queries/find/QueryVisitor.ts\";\nimport { isScope, isSelectAllFilter, resolveScope } from \"src/queries/find/scopes.ts\";\nimport { kq } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { fail } from \"src/utils.ts\";\n\nexport const findCountOperation = \"find-count\";\n\nexport function findCountDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n): Promise<number> {\n const { where, ...opts } = filter;\n if (opts.limit || opts.offset) {\n throw new Error(\"Cannot use limit/offset with findCountDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const pendingDeletedIds = appendPendingDeletedIds(em, type, query, meta.idDbType, where, opts);\n const { findSettings } = em[\"prepareFind\"](meta, findCountOperation, query, { ...opts, checkLimit: false });\n if (isQueryProvablyEmpty(query)) return Promise.resolve(0);\n\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, pendingDeletedIds, query, bindings, findSettings };\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<typeof prepared, number>(\n findCountOperation,\n batchKey,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings } = entries[0];\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query, findSettings, false);\n return [Number(rows[0].count)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT _find.tag, _data.count\n // FROM _find\n // CROSS JOIN LATERAL (\n // SELECT count(*) as count\n // FROM author a WHERE a.first_name = _find.arg1 OR a.last_name = _find.arg2\n // ) _data\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings } = entries[0];\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n\n // We're not returning the entities, just counting them...\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n\n const query2: ParsedFindQuery = {\n selects: [\"_find.tag as tag\", \"_data.count as count\"],\n tables: [\n { join: \"primary\", table: \"_find\", alias: \"_find\" },\n // Not sure what fromAlias is for/that it matters...\n { join: \"lateral\", query: query, table: meta.tableName, alias: \"_data\", fromAlias: \"_f\" },\n ],\n // For each unique query, capture its filter values in `bindings` to populate the CTE _find table\n ctes: [buildUnnestCte(\"_find\", argsColumns, createColumnValuesFromPrepared(args, entries))],\n orderBys: [],\n };\n\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query2, findSettings, false);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => 0);\n // Then put each row into the tagged query it matched\n for (const row of rows) {\n results[row.tag] = Number(row.count);\n }\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: (entry) => queryFilterHash(entry) },\n )\n .load(prepared);\n}\n\n/** Merges root-scope count/id settings with caller options, dropping pagination-only settings. */\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> } {\n if (!isScope<T>(where)) return { where, options };\n\n const resolved = resolveScope(where);\n const scopeOptions: FindCountFilterOptions<T> = {};\n if (resolved.softDeletes !== undefined) scopeOptions.softDeletes = resolved.softDeletes;\n return { where, options: { ...scopeOptions, ...options } };\n}\n\n/** Adds `id not in pendingDeletedIds` to the count query so pending deletes are excluded in SQL. */\nfunction appendPendingDeletedIds<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n query: ParsedFindQuery,\n idDbType: \"bigint\" | \"int\" | \"uuid\" | \"text\",\n where: FilterAndSettings<T>[\"where\"],\n opts: Omit<FilterAndSettings<T>, \"where\">,\n): readonly IdType[] {\n const pendingDeletedIds = isSelectAllFilter(where, opts.conditions)\n ? []\n : getEmInternalApi(em).pendingDeleteIds(type);\n if (pendingDeletedIds.length === 0) return pendingDeletedIds;\n\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n const condition = {\n kind: \"column\" as const,\n alias: primary.alias,\n column: \"id\",\n dbType: idDbType,\n cond: { kind: \"nin\" as const, value: pendingDeletedIds },\n pruneable: true,\n };\n\n if (!query.condition) {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [condition] };\n } else if (query.condition.op === \"and\") {\n query.condition.conditions.push(condition);\n } else {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [query.condition, condition] };\n }\n return pendingDeletedIds;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"findCountDataLoader.d.cts","names":[],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"mappings":";;;;;cAuBa;iBAEG,oBAAoB,UAAU,QAC5C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB;;iBAiFa,kBAAkB,UAAU,QAC1C,OAAO,WAAW,IAClB,SAAS,uBAAuB;EAC7B,OAAO,WAAW;EAAI,SAAS,uBAAuB;;iBAC3C,kBAAkB,UAAU,QAC1C,OAAO,WAAW,KAAK,uBAAuB,IAC9C,SAAS,uBAAuB;EAC7B,OAAO,WAAW,KAAK,uBAAuB;EAAI,SAAS,uBAAuB"}
1
+ {"version":3,"file":"findCountDataLoader.d.cts","names":[],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"mappings":";;;;;cAwBa;iBAEG,oBAAoB,UAAU,QAC5C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB;;iBAmFa,kBAAkB,UAAU,QAC1C,OAAO,WAAW,IAClB,SAAS,uBAAuB;EAC7B,OAAO,WAAW;EAAI,SAAS,uBAAuB;;iBAC3C,kBAAkB,UAAU,QAC1C,OAAO,WAAW,KAAK,uBAAuB,IAC9C,SAAS,uBAAuB;EAC7B,OAAO,WAAW,KAAK,uBAAuB;EAAI,SAAS,uBAAuB"}
@@ -1 +1 @@
1
- {"version":3,"file":"findCountDataLoader.d.mts","names":[],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"mappings":";;;;;cAuBa;iBAEG,oBAAoB,UAAU,QAC5C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB;;iBAiFa,kBAAkB,UAAU,QAC1C,OAAO,WAAW,IAClB,SAAS,uBAAuB;EAC7B,OAAO,WAAW;EAAI,SAAS,uBAAuB;;iBAC3C,kBAAkB,UAAU,QAC1C,OAAO,WAAW,KAAK,uBAAuB,IAC9C,SAAS,uBAAuB;EAC7B,OAAO,WAAW,KAAK,uBAAuB;EAAI,SAAS,uBAAuB"}
1
+ {"version":3,"file":"findCountDataLoader.d.mts","names":[],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"mappings":";;;;;cAwBa;iBAEG,oBAAoB,UAAU,QAC5C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB;;iBAmFa,kBAAkB,UAAU,QAC1C,OAAO,WAAW,IAClB,SAAS,uBAAuB;EAC7B,OAAO,WAAW;EAAI,SAAS,uBAAuB;;iBAC3C,kBAAkB,UAAU,QAC1C,OAAO,WAAW,KAAK,uBAAuB,IAC9C,SAAS,uBAAuB;EAC7B,OAAO,WAAW,KAAK,uBAAuB;EAAI,SAAS,uBAAuB"}
@@ -1,5 +1,6 @@
1
1
  import { kq } from "../queries/sql/keywords.js";
2
2
  import { isScope, isSelectAllFilter, resolveScope } from "../queries/find/scopes.js";
3
+ import { isQueryProvablyEmpty } from "../queries/find/QueryVisitor.js";
3
4
  import { parseFindQuery } from "../queries/find/QueryParser.js";
4
5
  import { fail } from "../utils.js";
5
6
  import { getMetadata } from "../EntityMetadata.js";
@@ -18,6 +19,7 @@ function findCountDataLoader(em, type, filter) {
18
19
  ...opts,
19
20
  checkLimit: false
20
21
  });
22
+ if (isQueryProvablyEmpty(query)) return Promise.resolve(0);
21
23
  const bindings = [];
22
24
  collectValues(bindings, query);
23
25
  const prepared = {
@@ -1 +1 @@
1
- {"version":3,"file":"findCountDataLoader.js","names":[],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"sourcesContent":["import {\n collectAndReplaceArgs,\n collectValues,\n createColumnValuesFromPrepared,\n getBatchKeyFromGenericStructure,\n queryFilterHash,\n} from \"src/dataloaders/findDataLoader.ts\";\nimport type { Entity, IdType } from \"src/Entity.ts\";\nimport {\n type EntityManager,\n type FindCountFilterOptions,\n type MaybeAbstractEntityConstructor,\n getEmInternalApi,\n} from \"src/EntityManager.ts\";\nimport { getMetadata } from \"src/EntityMetadata.ts\";\nimport type { FilterAndSettings, FindFilter } from \"src/queries/find/EntityFilter.ts\";\nimport type { GraphQLFilterWithAlias } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport { type ParsedFindQuery, parseFindQuery } from \"src/queries/find/QueryParser.ts\";\nimport { isScope, isSelectAllFilter, resolveScope } from \"src/queries/find/scopes.ts\";\nimport { kq } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { fail } from \"src/utils.ts\";\n\nexport const findCountOperation = \"find-count\";\n\nexport function findCountDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n): Promise<number> {\n const { where, ...opts } = filter;\n if (opts.limit || opts.offset) {\n throw new Error(\"Cannot use limit/offset with findCountDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const pendingDeletedIds = appendPendingDeletedIds(em, type, query, meta.idDbType, where, opts);\n const { findSettings } = em[\"prepareFind\"](meta, findCountOperation, query, { ...opts, checkLimit: false });\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, pendingDeletedIds, query, bindings, findSettings };\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<typeof prepared, number>(\n findCountOperation,\n batchKey,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings } = entries[0];\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query, findSettings, false);\n return [Number(rows[0].count)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT _find.tag, _data.count\n // FROM _find\n // CROSS JOIN LATERAL (\n // SELECT count(*) as count\n // FROM author a WHERE a.first_name = _find.arg1 OR a.last_name = _find.arg2\n // ) _data\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings } = entries[0];\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n\n // We're not returning the entities, just counting them...\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n\n const query2: ParsedFindQuery = {\n selects: [\"_find.tag as tag\", \"_data.count as count\"],\n tables: [\n { join: \"primary\", table: \"_find\", alias: \"_find\" },\n // Not sure what fromAlias is for/that it matters...\n { join: \"lateral\", query: query, table: meta.tableName, alias: \"_data\", fromAlias: \"_f\" },\n ],\n // For each unique query, capture its filter values in `bindings` to populate the CTE _find table\n ctes: [buildUnnestCte(\"_find\", argsColumns, createColumnValuesFromPrepared(args, entries))],\n orderBys: [],\n };\n\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query2, findSettings, false);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => 0);\n // Then put each row into the tagged query it matched\n for (const row of rows) {\n results[row.tag] = Number(row.count);\n }\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: (entry) => queryFilterHash(entry) },\n )\n .load(prepared);\n}\n\n/** Merges root-scope count/id settings with caller options, dropping pagination-only settings. */\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> } {\n if (!isScope<T>(where)) return { where, options };\n\n const resolved = resolveScope(where);\n const scopeOptions: FindCountFilterOptions<T> = {};\n if (resolved.softDeletes !== undefined) scopeOptions.softDeletes = resolved.softDeletes;\n return { where, options: { ...scopeOptions, ...options } };\n}\n\n/** Adds `id not in pendingDeletedIds` to the count query so pending deletes are excluded in SQL. */\nfunction appendPendingDeletedIds<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n query: ParsedFindQuery,\n idDbType: \"bigint\" | \"int\" | \"uuid\" | \"text\",\n where: FilterAndSettings<T>[\"where\"],\n opts: Omit<FilterAndSettings<T>, \"where\">,\n): readonly IdType[] {\n const pendingDeletedIds = isSelectAllFilter(where, opts.conditions)\n ? []\n : getEmInternalApi(em).pendingDeleteIds(type);\n if (pendingDeletedIds.length === 0) return pendingDeletedIds;\n\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n const condition = {\n kind: \"column\" as const,\n alias: primary.alias,\n column: \"id\",\n dbType: idDbType,\n cond: { kind: \"nin\" as const, value: pendingDeletedIds },\n pruneable: true,\n };\n\n if (!query.condition) {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [condition] };\n } else if (query.condition.op === \"and\") {\n query.condition.conditions.push(condition);\n } else {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [query.condition, condition] };\n }\n return pendingDeletedIds;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"findCountDataLoader.js","names":[],"sources":["../../src/dataloaders/findCountDataLoader.ts"],"sourcesContent":["import {\n collectAndReplaceArgs,\n collectValues,\n createColumnValuesFromPrepared,\n getBatchKeyFromGenericStructure,\n queryFilterHash,\n} from \"src/dataloaders/findDataLoader.ts\";\nimport type { Entity, IdType } from \"src/Entity.ts\";\nimport {\n type EntityManager,\n type FindCountFilterOptions,\n type MaybeAbstractEntityConstructor,\n getEmInternalApi,\n} from \"src/EntityManager.ts\";\nimport { getMetadata } from \"src/EntityMetadata.ts\";\nimport type { FilterAndSettings, FindFilter } from \"src/queries/find/EntityFilter.ts\";\nimport type { GraphQLFilterWithAlias } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport { type ParsedFindQuery, parseFindQuery } from \"src/queries/find/QueryParser.ts\";\nimport { isQueryProvablyEmpty } from \"src/queries/find/QueryVisitor.ts\";\nimport { isScope, isSelectAllFilter, resolveScope } from \"src/queries/find/scopes.ts\";\nimport { kq } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { fail } from \"src/utils.ts\";\n\nexport const findCountOperation = \"find-count\";\n\nexport function findCountDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n): Promise<number> {\n const { where, ...opts } = filter;\n if (opts.limit || opts.offset) {\n throw new Error(\"Cannot use limit/offset with findCountDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const pendingDeletedIds = appendPendingDeletedIds(em, type, query, meta.idDbType, where, opts);\n const { findSettings } = em[\"prepareFind\"](meta, findCountOperation, query, { ...opts, checkLimit: false });\n if (isQueryProvablyEmpty(query)) return Promise.resolve(0);\n\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, pendingDeletedIds, query, bindings, findSettings };\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<typeof prepared, number>(\n findCountOperation,\n batchKey,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings } = entries[0];\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query, findSettings, false);\n return [Number(rows[0].count)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT _find.tag, _data.count\n // FROM _find\n // CROSS JOIN LATERAL (\n // SELECT count(*) as count\n // FROM author a WHERE a.first_name = _find.arg1 OR a.last_name = _find.arg2\n // ) _data\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings } = entries[0];\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n\n // We're not returning the entities, just counting them...\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`count(distinct ${kq(primary.alias)}.id) as count`];\n query.orderBys = [];\n\n const query2: ParsedFindQuery = {\n selects: [\"_find.tag as tag\", \"_data.count as count\"],\n tables: [\n { join: \"primary\", table: \"_find\", alias: \"_find\" },\n // Not sure what fromAlias is for/that it matters...\n { join: \"lateral\", query: query, table: meta.tableName, alias: \"_data\", fromAlias: \"_f\" },\n ],\n // For each unique query, capture its filter values in `bindings` to populate the CTE _find table\n ctes: [buildUnnestCte(\"_find\", argsColumns, createColumnValuesFromPrepared(args, entries))],\n orderBys: [],\n };\n\n const rows = await em[\"executePreparedFind\"](meta, findCountOperation, query2, findSettings, false);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => 0);\n // Then put each row into the tagged query it matched\n for (const row of rows) {\n results[row.tag] = Number(row.count);\n }\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: (entry) => queryFilterHash(entry) },\n )\n .load(prepared);\n}\n\n/** Merges root-scope count/id settings with caller options, dropping pagination-only settings. */\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> };\nexport function mergeCountOptions<T extends Entity>(\n where: FindFilter<T> | GraphQLFilterWithAlias<T>,\n options: FindCountFilterOptions<T>,\n): { where: FindFilter<T> | GraphQLFilterWithAlias<T>; options: FindCountFilterOptions<T> } {\n if (!isScope<T>(where)) return { where, options };\n\n const resolved = resolveScope(where);\n const scopeOptions: FindCountFilterOptions<T> = {};\n if (resolved.softDeletes !== undefined) scopeOptions.softDeletes = resolved.softDeletes;\n return { where, options: { ...scopeOptions, ...options } };\n}\n\n/** Adds `id not in pendingDeletedIds` to the count query so pending deletes are excluded in SQL. */\nfunction appendPendingDeletedIds<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n query: ParsedFindQuery,\n idDbType: \"bigint\" | \"int\" | \"uuid\" | \"text\",\n where: FilterAndSettings<T>[\"where\"],\n opts: Omit<FilterAndSettings<T>, \"where\">,\n): readonly IdType[] {\n const pendingDeletedIds = isSelectAllFilter(where, opts.conditions)\n ? []\n : getEmInternalApi(em).pendingDeleteIds(type);\n if (pendingDeletedIds.length === 0) return pendingDeletedIds;\n\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n const condition = {\n kind: \"column\" as const,\n alias: primary.alias,\n column: \"id\",\n dbType: idDbType,\n cond: { kind: \"nin\" as const, value: pendingDeletedIds },\n pruneable: true,\n };\n\n if (!query.condition) {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [condition] };\n } else if (query.condition.op === \"and\") {\n query.condition.conditions.push(condition);\n } else {\n query.condition = { kind: \"exp\", op: \"and\", conditions: [query.condition, condition] };\n }\n return pendingDeletedIds;\n}\n"],"mappings":"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"}
@@ -22,6 +22,7 @@ function findDataLoader(em, type, filter, hint) {
22
22
  ...opts,
23
23
  limit: em.entityLimit
24
24
  });
25
+ if (require_queries_find_QueryVisitor.isQueryProvablyEmpty(query)) return Promise.resolve([]);
25
26
  const bindings = [];
26
27
  collectValues(bindings, query);
27
28
  const prepared = {
@@ -1 +1 @@
1
- {"version":3,"file":"findDataLoader.cjs","names":["getMetadata","parseFindQuery","hintKey","getEmInternalApi","buildHintTree","buildUnnestCte","getTables","fastWhereFilterHash","kqDot","parseAlias","opToFn","fail","equal","equalArrays"],"sources":["../../src/dataloaders/findDataLoader.ts"],"sourcesContent":["import { fastWhereFilterHash } from \"src/dataloaders/fastWhereFilterHash.ts\";\nimport type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, type MaybeAbstractEntityConstructor, getEmInternalApi } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { equal, equalArrays } from \"src/fields.ts\";\nimport { buildHintTree } from \"src/loading/HintTree.ts\";\nimport type { LoadHint } from \"src/loading/loadHints.ts\";\nimport { hintKey } from \"src/normalizeHints.ts\";\nimport type { FilterAndSettings } from \"src/queries/find/EntityFilter.ts\";\nimport { opToFn } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport {\n type ParsedCteClause,\n type ParsedFindQuery,\n type ParsedGroupBy,\n type ParsedSelect,\n getTables,\n parseAlias,\n parseFindQuery,\n} from \"src/queries/find/QueryParser.ts\";\nimport { visitConditions } from \"src/queries/find/QueryVisitor.ts\";\nimport type { ColumnCondition, ParsedValueFilter, RawCondition } from \"src/queries/parsedConditions.ts\";\nimport { kqDot } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { assertNever, fail } from \"src/utils.ts\";\n\nexport const findOperation = \"find\";\n\ninterface PreparedFindEntry<T extends Entity> {\n filter: FilterAndSettings<T>;\n query: ParsedFindQuery;\n bindings: any[];\n findSettings: any;\n checkLimit: boolean | undefined;\n}\n\nexport function findDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n hint: LoadHint<T> | undefined,\n): Promise<T[]> {\n const { where, ...opts } = filter;\n if (\"limit\" in opts || \"offset\" in opts) {\n throw new Error(\"Cannot use limit/offset with findDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const { checkLimit, findSettings } = em[\"prepareFind\"](meta, findOperation, query, {\n ...opts,\n limit: em.entityLimit,\n });\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, query, bindings, findSettings, checkLimit } satisfies PreparedFindEntry<T>;\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<PreparedFindEntry<T>, T[]>(\n findOperation,\n // It's unlikely we'll have simultaneous em.finds with the same WHERE clause structure\n // but lots of different load hints, and it'd be complicated to implement the preloading\n // in a way that doesn't naively over-fetch data (which our loadDataLoader does prevent,\n // but it's simpler b/c it's given exact ids to load), so for now just include the load\n // hint in the batch key.\n `${batchKey}-${hintKey(hint)}`,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings, checkLimit } = entries[0];\n // Maybe add preload joins\n const { preloader } = getEmInternalApi(em);\n const preloadHydrator = preloader && hint && preloader.addPreloading(meta, buildHintTree(hint), query);\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query, findSettings, checkLimit);\n const entities = em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => preloadHydrator?.(rowData, entities),\n });\n return [filterDeletedEntities(em, entities)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT a.*, array_agg(_find.tag) AS _tags\n // FROM authors a\n // CROSS JOIN _find AS _find\n // WHERE a.first_name = _find.arg0 OR a.last_name = _find.arg1\n // GROUP BY a.id\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings, checkLimit } = entries[0];\n const { preloader } = getEmInternalApi(em);\n const preloadJoins = preloader && hint && preloader.getPreloadJoins(meta, buildHintTree(hint), query);\n\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns: OpColumn[] = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n const columnValues = createColumnValuesFromPrepared(args, entries);\n const query2: ParsedFindQuery = {\n ...query,\n selects: [\"array_agg(_find.tag) as _tags\", ...query.selects],\n tables: [{ join: \"cross\", table: \"_find\", alias: \"_find\" }, ...query.tables],\n ctes: [buildUnnestCte(\"_find\", argsColumns, columnValues), ...(query.ctes ?? [])],\n };\n if (preloadJoins) {\n query2.selects.push(\n ...preloadJoins.flatMap((j) =>\n // Because we 'group by primary.id' to collapse the \"a1 matched multiple finds\" into\n // a single row, we also need to pick just the first value of each preload column\n j.selects.map((s) => `(array_agg(${s.value}))[1] AS ${s.as}`),\n ),\n );\n query2.tables.push(...preloadJoins.map((j) => j.join));\n }\n\n // Because we want to use `array_agg(tag)`, add `GROUP BY`s to the values we're selecting\n query2.groupBys = buildGroupBys(query2.selects);\n\n // Also because of our `array_agg` group by, add any order bys to the group by\n const [primary] = getTables(query2);\n for (const { alias, column } of query2.orderBys) {\n if (alias !== primary.alias) {\n query2.groupBys.push({ alias, column });\n }\n }\n\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query2, findSettings, checkLimit);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => [] as T[]);\n em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => {\n preloadJoins?.forEach((j) => j.hydrator(rowData, entities));\n // Put each row into the tagged query it matched\n for (let i = 0; i < entities.length; i++) {\n const entity = entities[i];\n if (!entity.isDeletedEntity) {\n for (const tag of rowData.get(i, \"_tags\")) results[tag].push(entity);\n }\n }\n },\n });\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: queryFilterHash },\n )\n .load(prepared);\n}\n\n/**\n * Returns a stable cache key from the already-parsed query rather than the raw `where`.\n *\n * A scope `where` is an opaque proxy that `fastWhereFilterHash` can't serialize — but it doesn't need\n * to: by this point the scope's structure and values are already worked into the `ParsedFindQuery`, so\n * hashing that distinguishes e.g. `Author.adult` (age>=18) from `Author.senior` (age>=65) correctly.\n */\nexport function queryFilterHash(entry: { query: ParsedFindQuery; findSettings: object }): any {\n return whereFilterHash({ query: entry.query, findSettings: entry.findSettings });\n}\n\nexport function whereFilterHash(where: object): any {\n const key = fastWhereFilterHash(where);\n if (key === undefined) throw new Error(\"fastWhereFilterHash could not serialize find filter\");\n return key;\n}\n\n/** Filters pending-delete entities in place to avoid allocating a replacement array for the common find path. */\nexport function filterDeletedEntities<T extends Entity>(em: EntityManager, entities: T[]): T[] {\n // Most EMs never delete entities, so skip even the findIndex scan until a delete has happened.\n if (!getEmInternalApi(em).hasAnyDeletes()) return entities;\n\n const firstDeleted = entities.findIndex((entity) => entity.isDeletedEntity);\n if (firstDeleted === -1) return entities;\n\n let writeIndex = firstDeleted;\n for (let readIndex = firstDeleted + 1; readIndex < entities.length; readIndex++) {\n const entity = entities[readIndex];\n if (!entity.isDeletedEntity) {\n entities[writeIndex++] = entity;\n }\n }\n entities.length = writeIndex;\n return entities;\n}\n\nclass ArgCounter {\n private index = 0;\n next(): number {\n return this.index++;\n }\n}\n\n/**\n * A single batched arg: the `_find` CTE column to create, plus the index into each entry's `bindings`\n * array that fills it. `bindingIndex` can differ from the column's position because inlined constants\n * are skipped — see {@link collectAndReplaceArgs}.\n */\nexport interface CteArg {\n column: OpColumn;\n bindingIndex: number;\n}\n\n/**\n * Rewrites each batched arg in `query` into a `_find.argX` placeholder.\n *\n * Any condition whose value(s) are identical across every batched query is left inline (using\n * query's condition as-is from entity0), so it renders as `col = ?` for better query planning.\n *\n * The returned array has the non-constant CTE columns, one {@link CteArg} per `_find.argX`.\n *\n * Because constants are skipped, the 1st CTE column (i.e. for last name) might actually have a\n * binding index of `1` if we've skipped an early constant column (i.e. first name).\n *\n * @param query the initial query in the batch, that all other queries structurally match\n * @param entries the values (bindings) for each query in the batch\n */\nexport function collectAndReplaceArgs(query: ParsedFindQuery, entries: readonly { bindings: any[] }[]): CteArg[] {\n const args: CteArg[] = [];\n const argsIndex = new ArgCounter();\n // `bindings` were pushed in this same `visitConditions` order, so we can walk the conditions and\n // each entry's `bindings` in lockstep, deciding per condition whether to inline or batch its value(s).\n const constant = computeConstantBindings(entries);\n let bindingIndex = 0;\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n const { kind } = c.cond;\n // `between` consumes two bindings, everything else (incl. array `in`/`nin`) consumes one.\n const consumed = kind === \"between\" ? 2 : 1;\n const start = bindingIndex;\n bindingIndex += consumed;\n // If every batched query shares this condition's value(s), leave it as a normal `col = ?`\n // condition (rendered from `entries[0]`'s value) instead of threading it through the CTE.\n let allConstant = true;\n for (let k = 0; k < consumed; k++) {\n if (!constant[start + k]) {\n allConstant = false;\n break;\n }\n }\n if (allConstant) return;\n // `arg${args.length}` keeps the CTE column names in step with the `_find.argX` references that\n // `rewriteToRawCondition` emits, since both only advance for the args we actually batch.\n if (kind === \"between\") {\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start });\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start + 1 });\n } else {\n // Figure out the _find CTE's `unnest($n::${dbType})` so Postgres knows what we're sending in\n const dbType =\n kind === \"in\" || kind === \"nin\"\n ? `${c.dbType}[]` // `in`/`nin` compare against an array column\n : kind === \"jsonPathExists\" || kind === \"jsonPathPredicate\"\n ? \"jsonpath\" // jsonPath needs `::jsonpath` (not `::jsonb`, which would be an invalid `jsonb @@ jsonb`)\n : c.dbType;\n args.push({ column: { columnName: `arg${args.length}`, dbType }, bindingIndex: start });\n }\n return rewriteToRawCondition(c, argsIndex);\n } else if (c.cond.kind === \"is-null\" || c.cond.kind === \"not-null\") {\n // leave it alone\n } else {\n throw new Error(\"Unsupported\");\n }\n },\n });\n return args;\n}\n\nfunction rewriteToRawCondition(c: ColumnCondition, argsIndex: ArgCounter): RawCondition {\n const [op, negate] = makeOp(c.cond, argsIndex);\n return {\n kind: \"raw\",\n aliases: [c.alias],\n condition: `${negate ? \"NOT \" : \"\"}${kqDot(c.alias, c.column)} ${op}`,\n pruneable: c.pruneable ?? false,\n bindings: [],\n };\n}\n\n/**\n * Builds the column-major values for the `_find` CTE.\n *\n * Column 0 is the per-entry `tag`; the remaining columns mirror `args` 1:1, each pulling its\n * `bindingIndex` from every entry's `bindings` (so inlined constants are naturally skipped).\n */\nexport function createColumnValuesFromPrepared(\n args: readonly CteArg[],\n entries: readonly { bindings: any[] }[],\n): any[][] {\n const columnValues: any[][] = new Array(args.length + 1);\n for (let i = 0; i < columnValues.length; i++) columnValues[i] = [];\n entries.forEach((entry, i) => {\n columnValues[0].push(i);\n for (let k = 0; k < args.length; k++) {\n columnValues[k + 1].push(entry.bindings[args[k].bindingIndex]);\n }\n });\n return columnValues;\n}\n\n/** Pushes the arg values of a given query in the cross-query `bindings` array. */\nexport function collectValues(bindings: any[], query: ParsedFindQuery): void {\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n // between has two values\n if (c.cond.kind === \"between\") {\n bindings.push(c.cond.value[0]);\n bindings.push(c.cond.value[1]);\n } else {\n bindings.push(c.cond.value);\n }\n }\n },\n });\n}\n\n/** Returns true for select expressions that do not need to be represented in GROUP BY. */\nfunction isAggregateSelect(select: string): boolean {\n return select.toLowerCase().includes(\"array_agg(\");\n}\n\n/** Builds GROUP BYs for selected values, accounting for whole-row selects that are grouped by primary key. */\nfunction buildGroupBys(selects: ParsedSelect[]): ParsedGroupBy[] {\n const sqlSelects = selects.map(selectSqlForGroupBy);\n const wholeRowAliases = new Set(\n sqlSelects\n .filter((select) => !isAggregateSelect(select) && stripSelectAlias(select).endsWith(\".*\"))\n .map(parseAlias),\n );\n return sqlSelects\n .filter((select) => !isAggregateSelect(select))\n .filter((select) => !isCoveredByWholeRowGroupBy(select, wholeRowAliases))\n .map((select) => groupBySelect(select));\n}\n\n/** Returns a SQL string for group-by analysis, until we support bindings in ParsedGroupBy. */\nfunction selectSqlForGroupBy(select: ParsedSelect): string {\n if (typeof select === \"string\") {\n return select;\n }\n throw new Error(`find batching does not support grouped selects with bindings: ${select.sql}`);\n}\n\n/** Builds a GROUP BY for a selected value so `array_agg(_find.tag)` can coexist with plugin-rewritten selects. */\nfunction groupBySelect(select: string): ParsedGroupBy {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return { alias: parseAlias(expression), column: \"id\" };\n }\n return { expression };\n}\n\n/** Removes a trailing SQL alias from a select expression, i.e. `(a.id) as id` -> `(a.id)`. */\nfunction stripSelectAlias(select: string): string {\n const match = /^(.*)\\s+as\\s+[^\\s]+$/i.exec(select);\n return match?.[1] ?? select;\n}\n\n/** Returns true when an expression is functionally covered by an already-grouped whole-row alias. */\nfunction isCoveredByWholeRowGroupBy(select: string, wholeRowAliases: Set<string>): boolean {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return false;\n }\n const aliases = findSelectedAliases(expression);\n return aliases.length > 0 && aliases.every((alias) => wholeRowAliases.has(alias));\n}\n\n/** Finds SQL aliases referenced as `alias.column` or `\"alias\".column`, i.e. `a.first_name` -> `a`. */\nfunction findSelectedAliases(expression: string): string[] {\n const aliases = new Set<string>();\n for (const match of expression.matchAll(/(?:\"([^\"]+)\"|([a-zA-Z_][\\w]*))\\./g)) {\n aliases.add(match[1] ?? match[2]);\n }\n return [...aliases];\n}\n\n/** Returns [operator, argsTaken, negate], i.e. `[\"=\", 1, false]`. */\nfunction makeOp(cond: ParsedValueFilter<any>, argsIndex: ArgCounter): [string, boolean] {\n switch (cond.kind) {\n case \"eq\":\n case \"ne\":\n case \"gte\":\n case \"gt\":\n case \"lte\":\n case \"lt\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\": {\n const fn = opToFn[cond.kind] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, false];\n }\n case \"noverlaps\":\n case \"ncontains\": {\n const fn = (opToFn as any)[cond.kind.substring(1)] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, true];\n }\n case \"is-null\":\n return [`IS NULL`, false];\n case \"not-null\":\n return [`IS NOT NULL`, false];\n case \"in\":\n return [`= ANY(_find.arg${argsIndex.next()})`, false];\n case \"nin\":\n return [`!= ALL(_find.arg${argsIndex.next()})`, false];\n case \"between\":\n return [`BETWEEN _find.arg${argsIndex.next()} AND _find.arg${argsIndex.next()}`, false];\n default:\n assertNever(cond);\n }\n}\n\n/**\n * Creates a `VALUES (...), (...)` SQL string for use in a CTE, typically for our\n * batch INSERTs/UPDATEs where the CTE is how we inject N rows of data/params into\n * the single INSERT/UPDATE statement.\n *\n * The caller is responsible for filling in the `bindings`, which should have 1\n * entry for each `rows x columns` cell.\n *\n * @deprecated Use newUnnestCte instead because it uses arrays to have a bounded number of parameters.\n */\nexport function buildValuesCte(\n alias: string,\n columns: { columnName: string; dbType: string }[],\n rows: readonly any[],\n // Should have a value for each `row x column` cell.\n bindings: readonly any[],\n): ParsedCteClause {\n return {\n alias,\n columns,\n query: {\n kind: \"raw\",\n sql: `VALUES\n ${rows.map((_, i) => `(${columns.map((c) => (i === 0 ? `?::${c.dbType}` : `?`)).join(\", \")})`).join(\", \")}\n `,\n bindings,\n },\n };\n}\n\n/** Returns, per binding slot, whether every batched query shares the same value. */\nfunction computeConstantBindings(entries: readonly { bindings: any[] }[]): boolean[] {\n const length = entries[0].bindings.length;\n const constant = new Array<boolean>(length);\n for (let j = 0; j < length; j++) {\n const first = entries[0].bindings[j];\n let same = true;\n for (let i = 1; i < entries.length; i++) {\n if (!argsEqual(entries[i].bindings[j], first)) {\n same = false;\n break;\n }\n }\n constant[j] = same;\n }\n return constant;\n}\n\n/**\n * Deep-equals two find-arg values (scalars, Dates, Temporals, or arrays thereof), reusing the same\n * scalar/array equality `setField` uses for change detection.\n *\n * Only used to decide whether a condition can be inlined, so false negatives are safe (we just fall\n * back to the CTE); we only return true when the values are genuinely equal.\n */\nfunction argsEqual(a: any, b: any): boolean {\n return equal(a, b) || (Array.isArray(a) && Array.isArray(b) && equalArrays(a, b));\n}\n\nexport function getBatchKeyFromGenericStructure(meta: EntityMetadata, query: ParsedFindQuery): string {\n // Temporarily swap condition values for `*` so we can see the generic structure of the query,\n // then restore them; this avoids deep-cloning the entire parsed query on every em.find call.\n // Track values per condition object b/c parseFindQuery does not deep copy complex conditions,\n // i.e. `a.firstName.eq(...)`, so the same condition instance could appear twice.\n const saved = new Map<any, any>();\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n const { cond } = c;\n if (\"value\" in cond && !saved.has(cond)) {\n saved.set(cond, cond.value);\n cond.value = \"*\";\n }\n },\n });\n const structure = JSON.stringify(query);\n for (const [cond, value] of saved) cond.value = value;\n // Include the meta b/c STI queries for different subtypes will look identical\n return meta.stiDiscriminatorValue ? `${structure}|${meta.type}` : structure;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"findDataLoader.cjs","names":["getMetadata","parseFindQuery","isQueryProvablyEmpty","hintKey","getEmInternalApi","buildHintTree","buildUnnestCte","getTables","fastWhereFilterHash","kqDot","parseAlias","opToFn","fail","equal","equalArrays"],"sources":["../../src/dataloaders/findDataLoader.ts"],"sourcesContent":["import { fastWhereFilterHash } from \"src/dataloaders/fastWhereFilterHash.ts\";\nimport type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, type MaybeAbstractEntityConstructor, getEmInternalApi } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { equal, equalArrays } from \"src/fields.ts\";\nimport { buildHintTree } from \"src/loading/HintTree.ts\";\nimport type { LoadHint } from \"src/loading/loadHints.ts\";\nimport { hintKey } from \"src/normalizeHints.ts\";\nimport type { FilterAndSettings } from \"src/queries/find/EntityFilter.ts\";\nimport { opToFn } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport {\n type ParsedCteClause,\n type ParsedFindQuery,\n type ParsedGroupBy,\n type ParsedSelect,\n getTables,\n parseAlias,\n parseFindQuery,\n} from \"src/queries/find/QueryParser.ts\";\nimport { isQueryProvablyEmpty, visitConditions } from \"src/queries/find/QueryVisitor.ts\";\nimport type { ColumnCondition, ParsedValueFilter, RawCondition } from \"src/queries/parsedConditions.ts\";\nimport { kqDot } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { assertNever, fail } from \"src/utils.ts\";\n\nexport const findOperation = \"find\";\n\ninterface PreparedFindEntry<T extends Entity> {\n filter: FilterAndSettings<T>;\n query: ParsedFindQuery;\n bindings: any[];\n findSettings: any;\n checkLimit: boolean | undefined;\n}\n\nexport function findDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n hint: LoadHint<T> | undefined,\n): Promise<T[]> {\n const { where, ...opts } = filter;\n if (\"limit\" in opts || \"offset\" in opts) {\n throw new Error(\"Cannot use limit/offset with findDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const { checkLimit, findSettings } = em[\"prepareFind\"](meta, findOperation, query, {\n ...opts,\n limit: em.entityLimit,\n });\n if (isQueryProvablyEmpty(query)) return Promise.resolve([]);\n\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, query, bindings, findSettings, checkLimit } satisfies PreparedFindEntry<T>;\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<PreparedFindEntry<T>, T[]>(\n findOperation,\n // It's unlikely we'll have simultaneous em.finds with the same WHERE clause structure\n // but lots of different load hints, and it'd be complicated to implement the preloading\n // in a way that doesn't naively over-fetch data (which our loadDataLoader does prevent,\n // but it's simpler b/c it's given exact ids to load), so for now just include the load\n // hint in the batch key.\n `${batchKey}-${hintKey(hint)}`,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings, checkLimit } = entries[0];\n // Maybe add preload joins\n const { preloader } = getEmInternalApi(em);\n const preloadHydrator = preloader && hint && preloader.addPreloading(meta, buildHintTree(hint), query);\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query, findSettings, checkLimit);\n const entities = em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => preloadHydrator?.(rowData, entities),\n });\n return [filterDeletedEntities(em, entities)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT a.*, array_agg(_find.tag) AS _tags\n // FROM authors a\n // CROSS JOIN _find AS _find\n // WHERE a.first_name = _find.arg0 OR a.last_name = _find.arg1\n // GROUP BY a.id\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings, checkLimit } = entries[0];\n const { preloader } = getEmInternalApi(em);\n const preloadJoins = preloader && hint && preloader.getPreloadJoins(meta, buildHintTree(hint), query);\n\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns: OpColumn[] = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n const columnValues = createColumnValuesFromPrepared(args, entries);\n const query2: ParsedFindQuery = {\n ...query,\n selects: [\"array_agg(_find.tag) as _tags\", ...query.selects],\n tables: [{ join: \"cross\", table: \"_find\", alias: \"_find\" }, ...query.tables],\n ctes: [buildUnnestCte(\"_find\", argsColumns, columnValues), ...(query.ctes ?? [])],\n };\n if (preloadJoins) {\n query2.selects.push(\n ...preloadJoins.flatMap((j) =>\n // Because we 'group by primary.id' to collapse the \"a1 matched multiple finds\" into\n // a single row, we also need to pick just the first value of each preload column\n j.selects.map((s) => `(array_agg(${s.value}))[1] AS ${s.as}`),\n ),\n );\n query2.tables.push(...preloadJoins.map((j) => j.join));\n }\n\n // Because we want to use `array_agg(tag)`, add `GROUP BY`s to the values we're selecting\n query2.groupBys = buildGroupBys(query2.selects);\n\n // Also because of our `array_agg` group by, add any order bys to the group by\n const [primary] = getTables(query2);\n for (const { alias, column } of query2.orderBys) {\n if (alias !== primary.alias) {\n query2.groupBys.push({ alias, column });\n }\n }\n\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query2, findSettings, checkLimit);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => [] as T[]);\n em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => {\n preloadJoins?.forEach((j) => j.hydrator(rowData, entities));\n // Put each row into the tagged query it matched\n for (let i = 0; i < entities.length; i++) {\n const entity = entities[i];\n if (!entity.isDeletedEntity) {\n for (const tag of rowData.get(i, \"_tags\")) results[tag].push(entity);\n }\n }\n },\n });\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: queryFilterHash },\n )\n .load(prepared);\n}\n\n/**\n * Returns a stable cache key from the already-parsed query rather than the raw `where`.\n *\n * A scope `where` is an opaque proxy that `fastWhereFilterHash` can't serialize — but it doesn't need\n * to: by this point the scope's structure and values are already worked into the `ParsedFindQuery`, so\n * hashing that distinguishes e.g. `Author.adult` (age>=18) from `Author.senior` (age>=65) correctly.\n */\nexport function queryFilterHash(entry: { query: ParsedFindQuery; findSettings: object }): any {\n return whereFilterHash({ query: entry.query, findSettings: entry.findSettings });\n}\n\nexport function whereFilterHash(where: object): any {\n const key = fastWhereFilterHash(where);\n if (key === undefined) throw new Error(\"fastWhereFilterHash could not serialize find filter\");\n return key;\n}\n\n/** Filters pending-delete entities in place to avoid allocating a replacement array for the common find path. */\nexport function filterDeletedEntities<T extends Entity>(em: EntityManager, entities: T[]): T[] {\n // Most EMs never delete entities, so skip even the findIndex scan until a delete has happened.\n if (!getEmInternalApi(em).hasAnyDeletes()) return entities;\n\n const firstDeleted = entities.findIndex((entity) => entity.isDeletedEntity);\n if (firstDeleted === -1) return entities;\n\n let writeIndex = firstDeleted;\n for (let readIndex = firstDeleted + 1; readIndex < entities.length; readIndex++) {\n const entity = entities[readIndex];\n if (!entity.isDeletedEntity) {\n entities[writeIndex++] = entity;\n }\n }\n entities.length = writeIndex;\n return entities;\n}\n\nclass ArgCounter {\n private index = 0;\n next(): number {\n return this.index++;\n }\n}\n\n/**\n * A single batched arg: the `_find` CTE column to create, plus the index into each entry's `bindings`\n * array that fills it. `bindingIndex` can differ from the column's position because inlined constants\n * are skipped — see {@link collectAndReplaceArgs}.\n */\nexport interface CteArg {\n column: OpColumn;\n bindingIndex: number;\n}\n\n/**\n * Rewrites each batched arg in `query` into a `_find.argX` placeholder.\n *\n * Any condition whose value(s) are identical across every batched query is left inline (using\n * query's condition as-is from entity0), so it renders as `col = ?` for better query planning.\n *\n * The returned array has the non-constant CTE columns, one {@link CteArg} per `_find.argX`.\n *\n * Because constants are skipped, the 1st CTE column (i.e. for last name) might actually have a\n * binding index of `1` if we've skipped an early constant column (i.e. first name).\n *\n * @param query the initial query in the batch, that all other queries structurally match\n * @param entries the values (bindings) for each query in the batch\n */\nexport function collectAndReplaceArgs(query: ParsedFindQuery, entries: readonly { bindings: any[] }[]): CteArg[] {\n const args: CteArg[] = [];\n const argsIndex = new ArgCounter();\n // `bindings` were pushed in this same `visitConditions` order, so we can walk the conditions and\n // each entry's `bindings` in lockstep, deciding per condition whether to inline or batch its value(s).\n const constant = computeConstantBindings(entries);\n let bindingIndex = 0;\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n const { kind } = c.cond;\n // `between` consumes two bindings, everything else (incl. array `in`/`nin`) consumes one.\n const consumed = kind === \"between\" ? 2 : 1;\n const start = bindingIndex;\n bindingIndex += consumed;\n // If every batched query shares this condition's value(s), leave it as a normal `col = ?`\n // condition (rendered from `entries[0]`'s value) instead of threading it through the CTE.\n let allConstant = true;\n for (let k = 0; k < consumed; k++) {\n if (!constant[start + k]) {\n allConstant = false;\n break;\n }\n }\n if (allConstant) return;\n // `arg${args.length}` keeps the CTE column names in step with the `_find.argX` references that\n // `rewriteToRawCondition` emits, since both only advance for the args we actually batch.\n if (kind === \"between\") {\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start });\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start + 1 });\n } else {\n // Figure out the _find CTE's `unnest($n::${dbType})` so Postgres knows what we're sending in\n const dbType =\n kind === \"in\" || kind === \"nin\"\n ? `${c.dbType}[]` // `in`/`nin` compare against an array column\n : kind === \"jsonPathExists\" || kind === \"jsonPathPredicate\"\n ? \"jsonpath\" // jsonPath needs `::jsonpath` (not `::jsonb`, which would be an invalid `jsonb @@ jsonb`)\n : c.dbType;\n args.push({ column: { columnName: `arg${args.length}`, dbType }, bindingIndex: start });\n }\n return rewriteToRawCondition(c, argsIndex);\n } else if (c.cond.kind === \"is-null\" || c.cond.kind === \"not-null\") {\n // leave it alone\n } else {\n throw new Error(\"Unsupported\");\n }\n },\n });\n return args;\n}\n\nfunction rewriteToRawCondition(c: ColumnCondition, argsIndex: ArgCounter): RawCondition {\n const [op, negate] = makeOp(c.cond, argsIndex);\n return {\n kind: \"raw\",\n aliases: [c.alias],\n condition: `${negate ? \"NOT \" : \"\"}${kqDot(c.alias, c.column)} ${op}`,\n pruneable: c.pruneable ?? false,\n bindings: [],\n };\n}\n\n/**\n * Builds the column-major values for the `_find` CTE.\n *\n * Column 0 is the per-entry `tag`; the remaining columns mirror `args` 1:1, each pulling its\n * `bindingIndex` from every entry's `bindings` (so inlined constants are naturally skipped).\n */\nexport function createColumnValuesFromPrepared(\n args: readonly CteArg[],\n entries: readonly { bindings: any[] }[],\n): any[][] {\n const columnValues: any[][] = new Array(args.length + 1);\n for (let i = 0; i < columnValues.length; i++) columnValues[i] = [];\n entries.forEach((entry, i) => {\n columnValues[0].push(i);\n for (let k = 0; k < args.length; k++) {\n columnValues[k + 1].push(entry.bindings[args[k].bindingIndex]);\n }\n });\n return columnValues;\n}\n\n/** Pushes the arg values of a given query in the cross-query `bindings` array. */\nexport function collectValues(bindings: any[], query: ParsedFindQuery): void {\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n // between has two values\n if (c.cond.kind === \"between\") {\n bindings.push(c.cond.value[0]);\n bindings.push(c.cond.value[1]);\n } else {\n bindings.push(c.cond.value);\n }\n }\n },\n });\n}\n\n/** Returns true for select expressions that do not need to be represented in GROUP BY. */\nfunction isAggregateSelect(select: string): boolean {\n return select.toLowerCase().includes(\"array_agg(\");\n}\n\n/** Builds GROUP BYs for selected values, accounting for whole-row selects that are grouped by primary key. */\nfunction buildGroupBys(selects: ParsedSelect[]): ParsedGroupBy[] {\n const sqlSelects = selects.map(selectSqlForGroupBy);\n const wholeRowAliases = new Set(\n sqlSelects\n .filter((select) => !isAggregateSelect(select) && stripSelectAlias(select).endsWith(\".*\"))\n .map(parseAlias),\n );\n return sqlSelects\n .filter((select) => !isAggregateSelect(select))\n .filter((select) => !isCoveredByWholeRowGroupBy(select, wholeRowAliases))\n .map((select) => groupBySelect(select));\n}\n\n/** Returns a SQL string for group-by analysis, until we support bindings in ParsedGroupBy. */\nfunction selectSqlForGroupBy(select: ParsedSelect): string {\n if (typeof select === \"string\") {\n return select;\n }\n throw new Error(`find batching does not support grouped selects with bindings: ${select.sql}`);\n}\n\n/** Builds a GROUP BY for a selected value so `array_agg(_find.tag)` can coexist with plugin-rewritten selects. */\nfunction groupBySelect(select: string): ParsedGroupBy {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return { alias: parseAlias(expression), column: \"id\" };\n }\n return { expression };\n}\n\n/** Removes a trailing SQL alias from a select expression, i.e. `(a.id) as id` -> `(a.id)`. */\nfunction stripSelectAlias(select: string): string {\n const match = /^(.*)\\s+as\\s+[^\\s]+$/i.exec(select);\n return match?.[1] ?? select;\n}\n\n/** Returns true when an expression is functionally covered by an already-grouped whole-row alias. */\nfunction isCoveredByWholeRowGroupBy(select: string, wholeRowAliases: Set<string>): boolean {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return false;\n }\n const aliases = findSelectedAliases(expression);\n return aliases.length > 0 && aliases.every((alias) => wholeRowAliases.has(alias));\n}\n\n/** Finds SQL aliases referenced as `alias.column` or `\"alias\".column`, i.e. `a.first_name` -> `a`. */\nfunction findSelectedAliases(expression: string): string[] {\n const aliases = new Set<string>();\n for (const match of expression.matchAll(/(?:\"([^\"]+)\"|([a-zA-Z_][\\w]*))\\./g)) {\n aliases.add(match[1] ?? match[2]);\n }\n return [...aliases];\n}\n\n/** Returns [operator, argsTaken, negate], i.e. `[\"=\", 1, false]`. */\nfunction makeOp(cond: ParsedValueFilter<any>, argsIndex: ArgCounter): [string, boolean] {\n switch (cond.kind) {\n case \"eq\":\n case \"ne\":\n case \"gte\":\n case \"gt\":\n case \"lte\":\n case \"lt\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\": {\n const fn = opToFn[cond.kind] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, false];\n }\n case \"noverlaps\":\n case \"ncontains\": {\n const fn = (opToFn as any)[cond.kind.substring(1)] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, true];\n }\n case \"is-null\":\n return [`IS NULL`, false];\n case \"not-null\":\n return [`IS NOT NULL`, false];\n case \"in\":\n return [`= ANY(_find.arg${argsIndex.next()})`, false];\n case \"nin\":\n return [`!= ALL(_find.arg${argsIndex.next()})`, false];\n case \"between\":\n return [`BETWEEN _find.arg${argsIndex.next()} AND _find.arg${argsIndex.next()}`, false];\n default:\n assertNever(cond);\n }\n}\n\n/**\n * Creates a `VALUES (...), (...)` SQL string for use in a CTE, typically for our\n * batch INSERTs/UPDATEs where the CTE is how we inject N rows of data/params into\n * the single INSERT/UPDATE statement.\n *\n * The caller is responsible for filling in the `bindings`, which should have 1\n * entry for each `rows x columns` cell.\n *\n * @deprecated Use newUnnestCte instead because it uses arrays to have a bounded number of parameters.\n */\nexport function buildValuesCte(\n alias: string,\n columns: { columnName: string; dbType: string }[],\n rows: readonly any[],\n // Should have a value for each `row x column` cell.\n bindings: readonly any[],\n): ParsedCteClause {\n return {\n alias,\n columns,\n query: {\n kind: \"raw\",\n sql: `VALUES\n ${rows.map((_, i) => `(${columns.map((c) => (i === 0 ? `?::${c.dbType}` : `?`)).join(\", \")})`).join(\", \")}\n `,\n bindings,\n },\n };\n}\n\n/** Returns, per binding slot, whether every batched query shares the same value. */\nfunction computeConstantBindings(entries: readonly { bindings: any[] }[]): boolean[] {\n const length = entries[0].bindings.length;\n const constant = new Array<boolean>(length);\n for (let j = 0; j < length; j++) {\n const first = entries[0].bindings[j];\n let same = true;\n for (let i = 1; i < entries.length; i++) {\n if (!argsEqual(entries[i].bindings[j], first)) {\n same = false;\n break;\n }\n }\n constant[j] = same;\n }\n return constant;\n}\n\n/**\n * Deep-equals two find-arg values (scalars, Dates, Temporals, or arrays thereof), reusing the same\n * scalar/array equality `setField` uses for change detection.\n *\n * Only used to decide whether a condition can be inlined, so false negatives are safe (we just fall\n * back to the CTE); we only return true when the values are genuinely equal.\n */\nfunction argsEqual(a: any, b: any): boolean {\n return equal(a, b) || (Array.isArray(a) && Array.isArray(b) && equalArrays(a, b));\n}\n\nexport function getBatchKeyFromGenericStructure(meta: EntityMetadata, query: ParsedFindQuery): string {\n // Temporarily swap condition values for `*` so we can see the generic structure of the query,\n // then restore them; this avoids deep-cloning the entire parsed query on every em.find call.\n // Track values per condition object b/c parseFindQuery does not deep copy complex conditions,\n // i.e. `a.firstName.eq(...)`, so the same condition instance could appear twice.\n const saved = new Map<any, any>();\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n const { cond } = c;\n if (\"value\" in cond && !saved.has(cond)) {\n saved.set(cond, cond.value);\n cond.value = \"*\";\n }\n },\n });\n const structure = JSON.stringify(query);\n for (const [cond, value] of saved) cond.value = value;\n // Include the meta b/c STI queries for different subtypes will look identical\n return meta.stiDiscriminatorValue ? `${structure}|${meta.type}` : structure;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"findDataLoader.d.cts","names":[],"sources":["../../src/dataloaders/findDataLoader.ts"],"mappings":";;;;;;;;cA0Ba;iBAUG,eAAe,UAAU,QACvC,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,IAC1B,MAAM,SAAS,iBACd,QAAQ;;;;;;;;iBAsHK,gBAAgB;EAAS,OAAO;EAAiB;;iBAIjD,gBAAgB;;iBAOhB,sBAAsB,UAAU,QAAQ,IAAI,eAAe,UAAU,MAAM;;;;;;UA8B1E;EACf,QAAQ;EACR;;;;;;;;;;;;;;;;iBAiBc,sBAAsB,OAAO,iBAAiB;EAAoB;MAAsB;;;;;;;iBAoExF,+BACd,eAAe,UACf;EAAoB;;;iBAcN,cAAc,iBAAiB,OAAO;;;;;;;;;;;iBAoItC,eACd,eACA;EAAW;EAAoB;KAC/B,sBAEA,2BACC;iBA2Ca,gCAAgC,MAAM,gBAAgB,OAAO"}
1
+ {"version":3,"file":"findDataLoader.d.cts","names":[],"sources":["../../src/dataloaders/findDataLoader.ts"],"mappings":";;;;;;;;cA0Ba;iBAUG,eAAe,UAAU,QACvC,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,IAC1B,MAAM,SAAS,iBACd,QAAQ;;;;;;;;iBAwHK,gBAAgB;EAAS,OAAO;EAAiB;;iBAIjD,gBAAgB;;iBAOhB,sBAAsB,UAAU,QAAQ,IAAI,eAAe,UAAU,MAAM;;;;;;UA8B1E;EACf,QAAQ;EACR;;;;;;;;;;;;;;;;iBAiBc,sBAAsB,OAAO,iBAAiB;EAAoB;MAAsB;;;;;;;iBAoExF,+BACd,eAAe,UACf;EAAoB;;;iBAcN,cAAc,iBAAiB,OAAO;;;;;;;;;;;iBAoItC,eACd,eACA;EAAW;EAAoB;KAC/B,sBAEA,2BACC;iBA2Ca,gCAAgC,MAAM,gBAAgB,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"findDataLoader.d.mts","names":[],"sources":["../../src/dataloaders/findDataLoader.ts"],"mappings":";;;;;;;;cA0Ba;iBAUG,eAAe,UAAU,QACvC,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,IAC1B,MAAM,SAAS,iBACd,QAAQ;;;;;;;;iBAsHK,gBAAgB;EAAS,OAAO;EAAiB;;iBAIjD,gBAAgB;;iBAOhB,sBAAsB,UAAU,QAAQ,IAAI,eAAe,UAAU,MAAM;;;;;;UA8B1E;EACf,QAAQ;EACR;;;;;;;;;;;;;;;;iBAiBc,sBAAsB,OAAO,iBAAiB;EAAoB;MAAsB;;;;;;;iBAoExF,+BACd,eAAe,UACf;EAAoB;;;iBAcN,cAAc,iBAAiB,OAAO;;;;;;;;;;;iBAoItC,eACd,eACA;EAAW;EAAoB;KAC/B,sBAEA,2BACC;iBA2Ca,gCAAgC,MAAM,gBAAgB,OAAO"}
1
+ {"version":3,"file":"findDataLoader.d.mts","names":[],"sources":["../../src/dataloaders/findDataLoader.ts"],"mappings":";;;;;;;;cA0Ba;iBAUG,eAAe,UAAU,QACvC,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,IAC1B,MAAM,SAAS,iBACd,QAAQ;;;;;;;;iBAwHK,gBAAgB;EAAS,OAAO;EAAiB;;iBAIjD,gBAAgB;;iBAOhB,sBAAsB,UAAU,QAAQ,IAAI,eAAe,UAAU,MAAM;;;;;;UA8B1E;EACf,QAAQ;EACR;;;;;;;;;;;;;;;;iBAiBc,sBAAsB,OAAO,iBAAiB;EAAoB;MAAsB;;;;;;;iBAoExF,+BACd,eAAe,UACf;EAAoB;;;iBAcN,cAAc,iBAAiB,OAAO;;;;;;;;;;;iBAoItC,eACd,eACA;EAAW;EAAoB;KAC/B,sBAEA,2BACC;iBA2Ca,gCAAgC,MAAM,gBAAgB,OAAO"}
@@ -1,7 +1,7 @@
1
1
  import { hintKey } from "../normalizeHints.js";
2
2
  import { equal, equalArrays } from "../fields.js";
3
3
  import { kqDot } from "../queries/sql/keywords.js";
4
- import { visitConditions } from "../queries/find/QueryVisitor.js";
4
+ import { isQueryProvablyEmpty, visitConditions } from "../queries/find/QueryVisitor.js";
5
5
  import { getTables, parseAlias, parseFindQuery } from "../queries/find/QueryParser.js";
6
6
  import { assertNever, fail } from "../utils.js";
7
7
  import { getMetadata } from "../EntityMetadata.js";
@@ -21,6 +21,7 @@ function findDataLoader(em, type, filter, hint) {
21
21
  ...opts,
22
22
  limit: em.entityLimit
23
23
  });
24
+ if (isQueryProvablyEmpty(query)) return Promise.resolve([]);
24
25
  const bindings = [];
25
26
  collectValues(bindings, query);
26
27
  const prepared = {
@@ -1 +1 @@
1
- {"version":3,"file":"findDataLoader.js","names":[],"sources":["../../src/dataloaders/findDataLoader.ts"],"sourcesContent":["import { fastWhereFilterHash } from \"src/dataloaders/fastWhereFilterHash.ts\";\nimport type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, type MaybeAbstractEntityConstructor, getEmInternalApi } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { equal, equalArrays } from \"src/fields.ts\";\nimport { buildHintTree } from \"src/loading/HintTree.ts\";\nimport type { LoadHint } from \"src/loading/loadHints.ts\";\nimport { hintKey } from \"src/normalizeHints.ts\";\nimport type { FilterAndSettings } from \"src/queries/find/EntityFilter.ts\";\nimport { opToFn } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport {\n type ParsedCteClause,\n type ParsedFindQuery,\n type ParsedGroupBy,\n type ParsedSelect,\n getTables,\n parseAlias,\n parseFindQuery,\n} from \"src/queries/find/QueryParser.ts\";\nimport { visitConditions } from \"src/queries/find/QueryVisitor.ts\";\nimport type { ColumnCondition, ParsedValueFilter, RawCondition } from \"src/queries/parsedConditions.ts\";\nimport { kqDot } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { assertNever, fail } from \"src/utils.ts\";\n\nexport const findOperation = \"find\";\n\ninterface PreparedFindEntry<T extends Entity> {\n filter: FilterAndSettings<T>;\n query: ParsedFindQuery;\n bindings: any[];\n findSettings: any;\n checkLimit: boolean | undefined;\n}\n\nexport function findDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n hint: LoadHint<T> | undefined,\n): Promise<T[]> {\n const { where, ...opts } = filter;\n if (\"limit\" in opts || \"offset\" in opts) {\n throw new Error(\"Cannot use limit/offset with findDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const { checkLimit, findSettings } = em[\"prepareFind\"](meta, findOperation, query, {\n ...opts,\n limit: em.entityLimit,\n });\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, query, bindings, findSettings, checkLimit } satisfies PreparedFindEntry<T>;\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<PreparedFindEntry<T>, T[]>(\n findOperation,\n // It's unlikely we'll have simultaneous em.finds with the same WHERE clause structure\n // but lots of different load hints, and it'd be complicated to implement the preloading\n // in a way that doesn't naively over-fetch data (which our loadDataLoader does prevent,\n // but it's simpler b/c it's given exact ids to load), so for now just include the load\n // hint in the batch key.\n `${batchKey}-${hintKey(hint)}`,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings, checkLimit } = entries[0];\n // Maybe add preload joins\n const { preloader } = getEmInternalApi(em);\n const preloadHydrator = preloader && hint && preloader.addPreloading(meta, buildHintTree(hint), query);\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query, findSettings, checkLimit);\n const entities = em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => preloadHydrator?.(rowData, entities),\n });\n return [filterDeletedEntities(em, entities)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT a.*, array_agg(_find.tag) AS _tags\n // FROM authors a\n // CROSS JOIN _find AS _find\n // WHERE a.first_name = _find.arg0 OR a.last_name = _find.arg1\n // GROUP BY a.id\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings, checkLimit } = entries[0];\n const { preloader } = getEmInternalApi(em);\n const preloadJoins = preloader && hint && preloader.getPreloadJoins(meta, buildHintTree(hint), query);\n\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns: OpColumn[] = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n const columnValues = createColumnValuesFromPrepared(args, entries);\n const query2: ParsedFindQuery = {\n ...query,\n selects: [\"array_agg(_find.tag) as _tags\", ...query.selects],\n tables: [{ join: \"cross\", table: \"_find\", alias: \"_find\" }, ...query.tables],\n ctes: [buildUnnestCte(\"_find\", argsColumns, columnValues), ...(query.ctes ?? [])],\n };\n if (preloadJoins) {\n query2.selects.push(\n ...preloadJoins.flatMap((j) =>\n // Because we 'group by primary.id' to collapse the \"a1 matched multiple finds\" into\n // a single row, we also need to pick just the first value of each preload column\n j.selects.map((s) => `(array_agg(${s.value}))[1] AS ${s.as}`),\n ),\n );\n query2.tables.push(...preloadJoins.map((j) => j.join));\n }\n\n // Because we want to use `array_agg(tag)`, add `GROUP BY`s to the values we're selecting\n query2.groupBys = buildGroupBys(query2.selects);\n\n // Also because of our `array_agg` group by, add any order bys to the group by\n const [primary] = getTables(query2);\n for (const { alias, column } of query2.orderBys) {\n if (alias !== primary.alias) {\n query2.groupBys.push({ alias, column });\n }\n }\n\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query2, findSettings, checkLimit);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => [] as T[]);\n em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => {\n preloadJoins?.forEach((j) => j.hydrator(rowData, entities));\n // Put each row into the tagged query it matched\n for (let i = 0; i < entities.length; i++) {\n const entity = entities[i];\n if (!entity.isDeletedEntity) {\n for (const tag of rowData.get(i, \"_tags\")) results[tag].push(entity);\n }\n }\n },\n });\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: queryFilterHash },\n )\n .load(prepared);\n}\n\n/**\n * Returns a stable cache key from the already-parsed query rather than the raw `where`.\n *\n * A scope `where` is an opaque proxy that `fastWhereFilterHash` can't serialize — but it doesn't need\n * to: by this point the scope's structure and values are already worked into the `ParsedFindQuery`, so\n * hashing that distinguishes e.g. `Author.adult` (age>=18) from `Author.senior` (age>=65) correctly.\n */\nexport function queryFilterHash(entry: { query: ParsedFindQuery; findSettings: object }): any {\n return whereFilterHash({ query: entry.query, findSettings: entry.findSettings });\n}\n\nexport function whereFilterHash(where: object): any {\n const key = fastWhereFilterHash(where);\n if (key === undefined) throw new Error(\"fastWhereFilterHash could not serialize find filter\");\n return key;\n}\n\n/** Filters pending-delete entities in place to avoid allocating a replacement array for the common find path. */\nexport function filterDeletedEntities<T extends Entity>(em: EntityManager, entities: T[]): T[] {\n // Most EMs never delete entities, so skip even the findIndex scan until a delete has happened.\n if (!getEmInternalApi(em).hasAnyDeletes()) return entities;\n\n const firstDeleted = entities.findIndex((entity) => entity.isDeletedEntity);\n if (firstDeleted === -1) return entities;\n\n let writeIndex = firstDeleted;\n for (let readIndex = firstDeleted + 1; readIndex < entities.length; readIndex++) {\n const entity = entities[readIndex];\n if (!entity.isDeletedEntity) {\n entities[writeIndex++] = entity;\n }\n }\n entities.length = writeIndex;\n return entities;\n}\n\nclass ArgCounter {\n private index = 0;\n next(): number {\n return this.index++;\n }\n}\n\n/**\n * A single batched arg: the `_find` CTE column to create, plus the index into each entry's `bindings`\n * array that fills it. `bindingIndex` can differ from the column's position because inlined constants\n * are skipped — see {@link collectAndReplaceArgs}.\n */\nexport interface CteArg {\n column: OpColumn;\n bindingIndex: number;\n}\n\n/**\n * Rewrites each batched arg in `query` into a `_find.argX` placeholder.\n *\n * Any condition whose value(s) are identical across every batched query is left inline (using\n * query's condition as-is from entity0), so it renders as `col = ?` for better query planning.\n *\n * The returned array has the non-constant CTE columns, one {@link CteArg} per `_find.argX`.\n *\n * Because constants are skipped, the 1st CTE column (i.e. for last name) might actually have a\n * binding index of `1` if we've skipped an early constant column (i.e. first name).\n *\n * @param query the initial query in the batch, that all other queries structurally match\n * @param entries the values (bindings) for each query in the batch\n */\nexport function collectAndReplaceArgs(query: ParsedFindQuery, entries: readonly { bindings: any[] }[]): CteArg[] {\n const args: CteArg[] = [];\n const argsIndex = new ArgCounter();\n // `bindings` were pushed in this same `visitConditions` order, so we can walk the conditions and\n // each entry's `bindings` in lockstep, deciding per condition whether to inline or batch its value(s).\n const constant = computeConstantBindings(entries);\n let bindingIndex = 0;\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n const { kind } = c.cond;\n // `between` consumes two bindings, everything else (incl. array `in`/`nin`) consumes one.\n const consumed = kind === \"between\" ? 2 : 1;\n const start = bindingIndex;\n bindingIndex += consumed;\n // If every batched query shares this condition's value(s), leave it as a normal `col = ?`\n // condition (rendered from `entries[0]`'s value) instead of threading it through the CTE.\n let allConstant = true;\n for (let k = 0; k < consumed; k++) {\n if (!constant[start + k]) {\n allConstant = false;\n break;\n }\n }\n if (allConstant) return;\n // `arg${args.length}` keeps the CTE column names in step with the `_find.argX` references that\n // `rewriteToRawCondition` emits, since both only advance for the args we actually batch.\n if (kind === \"between\") {\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start });\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start + 1 });\n } else {\n // Figure out the _find CTE's `unnest($n::${dbType})` so Postgres knows what we're sending in\n const dbType =\n kind === \"in\" || kind === \"nin\"\n ? `${c.dbType}[]` // `in`/`nin` compare against an array column\n : kind === \"jsonPathExists\" || kind === \"jsonPathPredicate\"\n ? \"jsonpath\" // jsonPath needs `::jsonpath` (not `::jsonb`, which would be an invalid `jsonb @@ jsonb`)\n : c.dbType;\n args.push({ column: { columnName: `arg${args.length}`, dbType }, bindingIndex: start });\n }\n return rewriteToRawCondition(c, argsIndex);\n } else if (c.cond.kind === \"is-null\" || c.cond.kind === \"not-null\") {\n // leave it alone\n } else {\n throw new Error(\"Unsupported\");\n }\n },\n });\n return args;\n}\n\nfunction rewriteToRawCondition(c: ColumnCondition, argsIndex: ArgCounter): RawCondition {\n const [op, negate] = makeOp(c.cond, argsIndex);\n return {\n kind: \"raw\",\n aliases: [c.alias],\n condition: `${negate ? \"NOT \" : \"\"}${kqDot(c.alias, c.column)} ${op}`,\n pruneable: c.pruneable ?? false,\n bindings: [],\n };\n}\n\n/**\n * Builds the column-major values for the `_find` CTE.\n *\n * Column 0 is the per-entry `tag`; the remaining columns mirror `args` 1:1, each pulling its\n * `bindingIndex` from every entry's `bindings` (so inlined constants are naturally skipped).\n */\nexport function createColumnValuesFromPrepared(\n args: readonly CteArg[],\n entries: readonly { bindings: any[] }[],\n): any[][] {\n const columnValues: any[][] = new Array(args.length + 1);\n for (let i = 0; i < columnValues.length; i++) columnValues[i] = [];\n entries.forEach((entry, i) => {\n columnValues[0].push(i);\n for (let k = 0; k < args.length; k++) {\n columnValues[k + 1].push(entry.bindings[args[k].bindingIndex]);\n }\n });\n return columnValues;\n}\n\n/** Pushes the arg values of a given query in the cross-query `bindings` array. */\nexport function collectValues(bindings: any[], query: ParsedFindQuery): void {\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n // between has two values\n if (c.cond.kind === \"between\") {\n bindings.push(c.cond.value[0]);\n bindings.push(c.cond.value[1]);\n } else {\n bindings.push(c.cond.value);\n }\n }\n },\n });\n}\n\n/** Returns true for select expressions that do not need to be represented in GROUP BY. */\nfunction isAggregateSelect(select: string): boolean {\n return select.toLowerCase().includes(\"array_agg(\");\n}\n\n/** Builds GROUP BYs for selected values, accounting for whole-row selects that are grouped by primary key. */\nfunction buildGroupBys(selects: ParsedSelect[]): ParsedGroupBy[] {\n const sqlSelects = selects.map(selectSqlForGroupBy);\n const wholeRowAliases = new Set(\n sqlSelects\n .filter((select) => !isAggregateSelect(select) && stripSelectAlias(select).endsWith(\".*\"))\n .map(parseAlias),\n );\n return sqlSelects\n .filter((select) => !isAggregateSelect(select))\n .filter((select) => !isCoveredByWholeRowGroupBy(select, wholeRowAliases))\n .map((select) => groupBySelect(select));\n}\n\n/** Returns a SQL string for group-by analysis, until we support bindings in ParsedGroupBy. */\nfunction selectSqlForGroupBy(select: ParsedSelect): string {\n if (typeof select === \"string\") {\n return select;\n }\n throw new Error(`find batching does not support grouped selects with bindings: ${select.sql}`);\n}\n\n/** Builds a GROUP BY for a selected value so `array_agg(_find.tag)` can coexist with plugin-rewritten selects. */\nfunction groupBySelect(select: string): ParsedGroupBy {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return { alias: parseAlias(expression), column: \"id\" };\n }\n return { expression };\n}\n\n/** Removes a trailing SQL alias from a select expression, i.e. `(a.id) as id` -> `(a.id)`. */\nfunction stripSelectAlias(select: string): string {\n const match = /^(.*)\\s+as\\s+[^\\s]+$/i.exec(select);\n return match?.[1] ?? select;\n}\n\n/** Returns true when an expression is functionally covered by an already-grouped whole-row alias. */\nfunction isCoveredByWholeRowGroupBy(select: string, wholeRowAliases: Set<string>): boolean {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return false;\n }\n const aliases = findSelectedAliases(expression);\n return aliases.length > 0 && aliases.every((alias) => wholeRowAliases.has(alias));\n}\n\n/** Finds SQL aliases referenced as `alias.column` or `\"alias\".column`, i.e. `a.first_name` -> `a`. */\nfunction findSelectedAliases(expression: string): string[] {\n const aliases = new Set<string>();\n for (const match of expression.matchAll(/(?:\"([^\"]+)\"|([a-zA-Z_][\\w]*))\\./g)) {\n aliases.add(match[1] ?? match[2]);\n }\n return [...aliases];\n}\n\n/** Returns [operator, argsTaken, negate], i.e. `[\"=\", 1, false]`. */\nfunction makeOp(cond: ParsedValueFilter<any>, argsIndex: ArgCounter): [string, boolean] {\n switch (cond.kind) {\n case \"eq\":\n case \"ne\":\n case \"gte\":\n case \"gt\":\n case \"lte\":\n case \"lt\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\": {\n const fn = opToFn[cond.kind] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, false];\n }\n case \"noverlaps\":\n case \"ncontains\": {\n const fn = (opToFn as any)[cond.kind.substring(1)] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, true];\n }\n case \"is-null\":\n return [`IS NULL`, false];\n case \"not-null\":\n return [`IS NOT NULL`, false];\n case \"in\":\n return [`= ANY(_find.arg${argsIndex.next()})`, false];\n case \"nin\":\n return [`!= ALL(_find.arg${argsIndex.next()})`, false];\n case \"between\":\n return [`BETWEEN _find.arg${argsIndex.next()} AND _find.arg${argsIndex.next()}`, false];\n default:\n assertNever(cond);\n }\n}\n\n/**\n * Creates a `VALUES (...), (...)` SQL string for use in a CTE, typically for our\n * batch INSERTs/UPDATEs where the CTE is how we inject N rows of data/params into\n * the single INSERT/UPDATE statement.\n *\n * The caller is responsible for filling in the `bindings`, which should have 1\n * entry for each `rows x columns` cell.\n *\n * @deprecated Use newUnnestCte instead because it uses arrays to have a bounded number of parameters.\n */\nexport function buildValuesCte(\n alias: string,\n columns: { columnName: string; dbType: string }[],\n rows: readonly any[],\n // Should have a value for each `row x column` cell.\n bindings: readonly any[],\n): ParsedCteClause {\n return {\n alias,\n columns,\n query: {\n kind: \"raw\",\n sql: `VALUES\n ${rows.map((_, i) => `(${columns.map((c) => (i === 0 ? `?::${c.dbType}` : `?`)).join(\", \")})`).join(\", \")}\n `,\n bindings,\n },\n };\n}\n\n/** Returns, per binding slot, whether every batched query shares the same value. */\nfunction computeConstantBindings(entries: readonly { bindings: any[] }[]): boolean[] {\n const length = entries[0].bindings.length;\n const constant = new Array<boolean>(length);\n for (let j = 0; j < length; j++) {\n const first = entries[0].bindings[j];\n let same = true;\n for (let i = 1; i < entries.length; i++) {\n if (!argsEqual(entries[i].bindings[j], first)) {\n same = false;\n break;\n }\n }\n constant[j] = same;\n }\n return constant;\n}\n\n/**\n * Deep-equals two find-arg values (scalars, Dates, Temporals, or arrays thereof), reusing the same\n * scalar/array equality `setField` uses for change detection.\n *\n * Only used to decide whether a condition can be inlined, so false negatives are safe (we just fall\n * back to the CTE); we only return true when the values are genuinely equal.\n */\nfunction argsEqual(a: any, b: any): boolean {\n return equal(a, b) || (Array.isArray(a) && Array.isArray(b) && equalArrays(a, b));\n}\n\nexport function getBatchKeyFromGenericStructure(meta: EntityMetadata, query: ParsedFindQuery): string {\n // Temporarily swap condition values for `*` so we can see the generic structure of the query,\n // then restore them; this avoids deep-cloning the entire parsed query on every em.find call.\n // Track values per condition object b/c parseFindQuery does not deep copy complex conditions,\n // i.e. `a.firstName.eq(...)`, so the same condition instance could appear twice.\n const saved = new Map<any, any>();\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n const { cond } = c;\n if (\"value\" in cond && !saved.has(cond)) {\n saved.set(cond, cond.value);\n cond.value = \"*\";\n }\n },\n });\n const structure = JSON.stringify(query);\n for (const [cond, value] of saved) cond.value = value;\n // Include the meta b/c STI queries for different subtypes will look identical\n return meta.stiDiscriminatorValue ? `${structure}|${meta.type}` : structure;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"findDataLoader.js","names":[],"sources":["../../src/dataloaders/findDataLoader.ts"],"sourcesContent":["import { fastWhereFilterHash } from \"src/dataloaders/fastWhereFilterHash.ts\";\nimport type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, type MaybeAbstractEntityConstructor, getEmInternalApi } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { equal, equalArrays } from \"src/fields.ts\";\nimport { buildHintTree } from \"src/loading/HintTree.ts\";\nimport type { LoadHint } from \"src/loading/loadHints.ts\";\nimport { hintKey } from \"src/normalizeHints.ts\";\nimport type { FilterAndSettings } from \"src/queries/find/EntityFilter.ts\";\nimport { opToFn } from \"src/queries/find/EntityGraphQLFilter.ts\";\nimport {\n type ParsedCteClause,\n type ParsedFindQuery,\n type ParsedGroupBy,\n type ParsedSelect,\n getTables,\n parseAlias,\n parseFindQuery,\n} from \"src/queries/find/QueryParser.ts\";\nimport { isQueryProvablyEmpty, visitConditions } from \"src/queries/find/QueryVisitor.ts\";\nimport type { ColumnCondition, ParsedValueFilter, RawCondition } from \"src/queries/parsedConditions.ts\";\nimport { kqDot } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { assertNever, fail } from \"src/utils.ts\";\n\nexport const findOperation = \"find\";\n\ninterface PreparedFindEntry<T extends Entity> {\n filter: FilterAndSettings<T>;\n query: ParsedFindQuery;\n bindings: any[];\n findSettings: any;\n checkLimit: boolean | undefined;\n}\n\nexport function findDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n hint: LoadHint<T> | undefined,\n): Promise<T[]> {\n const { where, ...opts } = filter;\n if (\"limit\" in opts || \"offset\" in opts) {\n throw new Error(\"Cannot use limit/offset with findDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const { checkLimit, findSettings } = em[\"prepareFind\"](meta, findOperation, query, {\n ...opts,\n limit: em.entityLimit,\n });\n if (isQueryProvablyEmpty(query)) return Promise.resolve([]);\n\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, query, bindings, findSettings, checkLimit } satisfies PreparedFindEntry<T>;\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<PreparedFindEntry<T>, T[]>(\n findOperation,\n // It's unlikely we'll have simultaneous em.finds with the same WHERE clause structure\n // but lots of different load hints, and it'd be complicated to implement the preloading\n // in a way that doesn't naively over-fetch data (which our loadDataLoader does prevent,\n // but it's simpler b/c it's given exact ids to load), so for now just include the load\n // hint in the batch key.\n `${batchKey}-${hintKey(hint)}`,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings, checkLimit } = entries[0];\n // Maybe add preload joins\n const { preloader } = getEmInternalApi(em);\n const preloadHydrator = preloader && hint && preloader.addPreloading(meta, buildHintTree(hint), query);\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query, findSettings, checkLimit);\n const entities = em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => preloadHydrator?.(rowData, entities),\n });\n return [filterDeletedEntities(em, entities)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]), unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT a.*, array_agg(_find.tag) AS _tags\n // FROM authors a\n // CROSS JOIN _find AS _find\n // WHERE a.first_name = _find.arg0 OR a.last_name = _find.arg1\n // GROUP BY a.id\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings, checkLimit } = entries[0];\n const { preloader } = getEmInternalApi(em);\n const preloadJoins = preloader && hint && preloader.getPreloadJoins(meta, buildHintTree(hint), query);\n\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns: OpColumn[] = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n const columnValues = createColumnValuesFromPrepared(args, entries);\n const query2: ParsedFindQuery = {\n ...query,\n selects: [\"array_agg(_find.tag) as _tags\", ...query.selects],\n tables: [{ join: \"cross\", table: \"_find\", alias: \"_find\" }, ...query.tables],\n ctes: [buildUnnestCte(\"_find\", argsColumns, columnValues), ...(query.ctes ?? [])],\n };\n if (preloadJoins) {\n query2.selects.push(\n ...preloadJoins.flatMap((j) =>\n // Because we 'group by primary.id' to collapse the \"a1 matched multiple finds\" into\n // a single row, we also need to pick just the first value of each preload column\n j.selects.map((s) => `(array_agg(${s.value}))[1] AS ${s.as}`),\n ),\n );\n query2.tables.push(...preloadJoins.map((j) => j.join));\n }\n\n // Because we want to use `array_agg(tag)`, add `GROUP BY`s to the values we're selecting\n query2.groupBys = buildGroupBys(query2.selects);\n\n // Also because of our `array_agg` group by, add any order bys to the group by\n const [primary] = getTables(query2);\n for (const { alias, column } of query2.orderBys) {\n if (alias !== primary.alias) {\n query2.groupBys.push({ alias, column });\n }\n }\n\n const rowData = await em[\"executePreparedFindRowData\"](meta, findOperation, query2, findSettings, checkLimit);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results = entries.map(() => [] as T[]);\n em[\"hydrateAndFinalize\"](type, rowData, {\n sidecars: (entities) => {\n preloadJoins?.forEach((j) => j.hydrator(rowData, entities));\n // Put each row into the tagged query it matched\n for (let i = 0; i < entities.length; i++) {\n const entity = entities[i];\n if (!entity.isDeletedEntity) {\n for (const tag of rowData.get(i, \"_tags\")) results[tag].push(entity);\n }\n }\n },\n });\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: queryFilterHash },\n )\n .load(prepared);\n}\n\n/**\n * Returns a stable cache key from the already-parsed query rather than the raw `where`.\n *\n * A scope `where` is an opaque proxy that `fastWhereFilterHash` can't serialize — but it doesn't need\n * to: by this point the scope's structure and values are already worked into the `ParsedFindQuery`, so\n * hashing that distinguishes e.g. `Author.adult` (age>=18) from `Author.senior` (age>=65) correctly.\n */\nexport function queryFilterHash(entry: { query: ParsedFindQuery; findSettings: object }): any {\n return whereFilterHash({ query: entry.query, findSettings: entry.findSettings });\n}\n\nexport function whereFilterHash(where: object): any {\n const key = fastWhereFilterHash(where);\n if (key === undefined) throw new Error(\"fastWhereFilterHash could not serialize find filter\");\n return key;\n}\n\n/** Filters pending-delete entities in place to avoid allocating a replacement array for the common find path. */\nexport function filterDeletedEntities<T extends Entity>(em: EntityManager, entities: T[]): T[] {\n // Most EMs never delete entities, so skip even the findIndex scan until a delete has happened.\n if (!getEmInternalApi(em).hasAnyDeletes()) return entities;\n\n const firstDeleted = entities.findIndex((entity) => entity.isDeletedEntity);\n if (firstDeleted === -1) return entities;\n\n let writeIndex = firstDeleted;\n for (let readIndex = firstDeleted + 1; readIndex < entities.length; readIndex++) {\n const entity = entities[readIndex];\n if (!entity.isDeletedEntity) {\n entities[writeIndex++] = entity;\n }\n }\n entities.length = writeIndex;\n return entities;\n}\n\nclass ArgCounter {\n private index = 0;\n next(): number {\n return this.index++;\n }\n}\n\n/**\n * A single batched arg: the `_find` CTE column to create, plus the index into each entry's `bindings`\n * array that fills it. `bindingIndex` can differ from the column's position because inlined constants\n * are skipped — see {@link collectAndReplaceArgs}.\n */\nexport interface CteArg {\n column: OpColumn;\n bindingIndex: number;\n}\n\n/**\n * Rewrites each batched arg in `query` into a `_find.argX` placeholder.\n *\n * Any condition whose value(s) are identical across every batched query is left inline (using\n * query's condition as-is from entity0), so it renders as `col = ?` for better query planning.\n *\n * The returned array has the non-constant CTE columns, one {@link CteArg} per `_find.argX`.\n *\n * Because constants are skipped, the 1st CTE column (i.e. for last name) might actually have a\n * binding index of `1` if we've skipped an early constant column (i.e. first name).\n *\n * @param query the initial query in the batch, that all other queries structurally match\n * @param entries the values (bindings) for each query in the batch\n */\nexport function collectAndReplaceArgs(query: ParsedFindQuery, entries: readonly { bindings: any[] }[]): CteArg[] {\n const args: CteArg[] = [];\n const argsIndex = new ArgCounter();\n // `bindings` were pushed in this same `visitConditions` order, so we can walk the conditions and\n // each entry's `bindings` in lockstep, deciding per condition whether to inline or batch its value(s).\n const constant = computeConstantBindings(entries);\n let bindingIndex = 0;\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n const { kind } = c.cond;\n // `between` consumes two bindings, everything else (incl. array `in`/`nin`) consumes one.\n const consumed = kind === \"between\" ? 2 : 1;\n const start = bindingIndex;\n bindingIndex += consumed;\n // If every batched query shares this condition's value(s), leave it as a normal `col = ?`\n // condition (rendered from `entries[0]`'s value) instead of threading it through the CTE.\n let allConstant = true;\n for (let k = 0; k < consumed; k++) {\n if (!constant[start + k]) {\n allConstant = false;\n break;\n }\n }\n if (allConstant) return;\n // `arg${args.length}` keeps the CTE column names in step with the `_find.argX` references that\n // `rewriteToRawCondition` emits, since both only advance for the args we actually batch.\n if (kind === \"between\") {\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start });\n args.push({ column: { columnName: `arg${args.length}`, dbType: c.dbType }, bindingIndex: start + 1 });\n } else {\n // Figure out the _find CTE's `unnest($n::${dbType})` so Postgres knows what we're sending in\n const dbType =\n kind === \"in\" || kind === \"nin\"\n ? `${c.dbType}[]` // `in`/`nin` compare against an array column\n : kind === \"jsonPathExists\" || kind === \"jsonPathPredicate\"\n ? \"jsonpath\" // jsonPath needs `::jsonpath` (not `::jsonb`, which would be an invalid `jsonb @@ jsonb`)\n : c.dbType;\n args.push({ column: { columnName: `arg${args.length}`, dbType }, bindingIndex: start });\n }\n return rewriteToRawCondition(c, argsIndex);\n } else if (c.cond.kind === \"is-null\" || c.cond.kind === \"not-null\") {\n // leave it alone\n } else {\n throw new Error(\"Unsupported\");\n }\n },\n });\n return args;\n}\n\nfunction rewriteToRawCondition(c: ColumnCondition, argsIndex: ArgCounter): RawCondition {\n const [op, negate] = makeOp(c.cond, argsIndex);\n return {\n kind: \"raw\",\n aliases: [c.alias],\n condition: `${negate ? \"NOT \" : \"\"}${kqDot(c.alias, c.column)} ${op}`,\n pruneable: c.pruneable ?? false,\n bindings: [],\n };\n}\n\n/**\n * Builds the column-major values for the `_find` CTE.\n *\n * Column 0 is the per-entry `tag`; the remaining columns mirror `args` 1:1, each pulling its\n * `bindingIndex` from every entry's `bindings` (so inlined constants are naturally skipped).\n */\nexport function createColumnValuesFromPrepared(\n args: readonly CteArg[],\n entries: readonly { bindings: any[] }[],\n): any[][] {\n const columnValues: any[][] = new Array(args.length + 1);\n for (let i = 0; i < columnValues.length; i++) columnValues[i] = [];\n entries.forEach((entry, i) => {\n columnValues[0].push(i);\n for (let k = 0; k < args.length; k++) {\n columnValues[k + 1].push(entry.bindings[args[k].bindingIndex]);\n }\n });\n return columnValues;\n}\n\n/** Pushes the arg values of a given query in the cross-query `bindings` array. */\nexport function collectValues(bindings: any[], query: ParsedFindQuery): void {\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n if (\"value\" in c.cond) {\n // between has two values\n if (c.cond.kind === \"between\") {\n bindings.push(c.cond.value[0]);\n bindings.push(c.cond.value[1]);\n } else {\n bindings.push(c.cond.value);\n }\n }\n },\n });\n}\n\n/** Returns true for select expressions that do not need to be represented in GROUP BY. */\nfunction isAggregateSelect(select: string): boolean {\n return select.toLowerCase().includes(\"array_agg(\");\n}\n\n/** Builds GROUP BYs for selected values, accounting for whole-row selects that are grouped by primary key. */\nfunction buildGroupBys(selects: ParsedSelect[]): ParsedGroupBy[] {\n const sqlSelects = selects.map(selectSqlForGroupBy);\n const wholeRowAliases = new Set(\n sqlSelects\n .filter((select) => !isAggregateSelect(select) && stripSelectAlias(select).endsWith(\".*\"))\n .map(parseAlias),\n );\n return sqlSelects\n .filter((select) => !isAggregateSelect(select))\n .filter((select) => !isCoveredByWholeRowGroupBy(select, wholeRowAliases))\n .map((select) => groupBySelect(select));\n}\n\n/** Returns a SQL string for group-by analysis, until we support bindings in ParsedGroupBy. */\nfunction selectSqlForGroupBy(select: ParsedSelect): string {\n if (typeof select === \"string\") {\n return select;\n }\n throw new Error(`find batching does not support grouped selects with bindings: ${select.sql}`);\n}\n\n/** Builds a GROUP BY for a selected value so `array_agg(_find.tag)` can coexist with plugin-rewritten selects. */\nfunction groupBySelect(select: string): ParsedGroupBy {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return { alias: parseAlias(expression), column: \"id\" };\n }\n return { expression };\n}\n\n/** Removes a trailing SQL alias from a select expression, i.e. `(a.id) as id` -> `(a.id)`. */\nfunction stripSelectAlias(select: string): string {\n const match = /^(.*)\\s+as\\s+[^\\s]+$/i.exec(select);\n return match?.[1] ?? select;\n}\n\n/** Returns true when an expression is functionally covered by an already-grouped whole-row alias. */\nfunction isCoveredByWholeRowGroupBy(select: string, wholeRowAliases: Set<string>): boolean {\n const expression = stripSelectAlias(select);\n if (expression.endsWith(\".*\")) {\n return false;\n }\n const aliases = findSelectedAliases(expression);\n return aliases.length > 0 && aliases.every((alias) => wholeRowAliases.has(alias));\n}\n\n/** Finds SQL aliases referenced as `alias.column` or `\"alias\".column`, i.e. `a.first_name` -> `a`. */\nfunction findSelectedAliases(expression: string): string[] {\n const aliases = new Set<string>();\n for (const match of expression.matchAll(/(?:\"([^\"]+)\"|([a-zA-Z_][\\w]*))\\./g)) {\n aliases.add(match[1] ?? match[2]);\n }\n return [...aliases];\n}\n\n/** Returns [operator, argsTaken, negate], i.e. `[\"=\", 1, false]`. */\nfunction makeOp(cond: ParsedValueFilter<any>, argsIndex: ArgCounter): [string, boolean] {\n switch (cond.kind) {\n case \"eq\":\n case \"ne\":\n case \"gte\":\n case \"gt\":\n case \"lte\":\n case \"lt\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\": {\n const fn = opToFn[cond.kind] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, false];\n }\n case \"noverlaps\":\n case \"ncontains\": {\n const fn = (opToFn as any)[cond.kind.substring(1)] ?? fail(`Invalid operator ${cond.kind}`);\n return [`${fn} _find.arg${argsIndex.next()}`, true];\n }\n case \"is-null\":\n return [`IS NULL`, false];\n case \"not-null\":\n return [`IS NOT NULL`, false];\n case \"in\":\n return [`= ANY(_find.arg${argsIndex.next()})`, false];\n case \"nin\":\n return [`!= ALL(_find.arg${argsIndex.next()})`, false];\n case \"between\":\n return [`BETWEEN _find.arg${argsIndex.next()} AND _find.arg${argsIndex.next()}`, false];\n default:\n assertNever(cond);\n }\n}\n\n/**\n * Creates a `VALUES (...), (...)` SQL string for use in a CTE, typically for our\n * batch INSERTs/UPDATEs where the CTE is how we inject N rows of data/params into\n * the single INSERT/UPDATE statement.\n *\n * The caller is responsible for filling in the `bindings`, which should have 1\n * entry for each `rows x columns` cell.\n *\n * @deprecated Use newUnnestCte instead because it uses arrays to have a bounded number of parameters.\n */\nexport function buildValuesCte(\n alias: string,\n columns: { columnName: string; dbType: string }[],\n rows: readonly any[],\n // Should have a value for each `row x column` cell.\n bindings: readonly any[],\n): ParsedCteClause {\n return {\n alias,\n columns,\n query: {\n kind: \"raw\",\n sql: `VALUES\n ${rows.map((_, i) => `(${columns.map((c) => (i === 0 ? `?::${c.dbType}` : `?`)).join(\", \")})`).join(\", \")}\n `,\n bindings,\n },\n };\n}\n\n/** Returns, per binding slot, whether every batched query shares the same value. */\nfunction computeConstantBindings(entries: readonly { bindings: any[] }[]): boolean[] {\n const length = entries[0].bindings.length;\n const constant = new Array<boolean>(length);\n for (let j = 0; j < length; j++) {\n const first = entries[0].bindings[j];\n let same = true;\n for (let i = 1; i < entries.length; i++) {\n if (!argsEqual(entries[i].bindings[j], first)) {\n same = false;\n break;\n }\n }\n constant[j] = same;\n }\n return constant;\n}\n\n/**\n * Deep-equals two find-arg values (scalars, Dates, Temporals, or arrays thereof), reusing the same\n * scalar/array equality `setField` uses for change detection.\n *\n * Only used to decide whether a condition can be inlined, so false negatives are safe (we just fall\n * back to the CTE); we only return true when the values are genuinely equal.\n */\nfunction argsEqual(a: any, b: any): boolean {\n return equal(a, b) || (Array.isArray(a) && Array.isArray(b) && equalArrays(a, b));\n}\n\nexport function getBatchKeyFromGenericStructure(meta: EntityMetadata, query: ParsedFindQuery): string {\n // Temporarily swap condition values for `*` so we can see the generic structure of the query,\n // then restore them; this avoids deep-cloning the entire parsed query on every em.find call.\n // Track values per condition object b/c parseFindQuery does not deep copy complex conditions,\n // i.e. `a.firstName.eq(...)`, so the same condition instance could appear twice.\n const saved = new Map<any, any>();\n visitConditions(query, {\n visitCond(c: ColumnCondition) {\n const { cond } = c;\n if (\"value\" in cond && !saved.has(cond)) {\n saved.set(cond, cond.value);\n cond.value = \"*\";\n }\n },\n });\n const structure = JSON.stringify(query);\n for (const [cond, value] of saved) cond.value = value;\n // Include the meta b/c STI queries for different subtypes will look identical\n return meta.stiDiscriminatorValue ? `${structure}|${meta.type}` : structure;\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_keys = require("../keys.cjs");
3
3
  const require_queries_sql_keywords = require("../queries/sql/keywords.cjs");
4
+ const require_queries_find_QueryVisitor = require("../queries/find/QueryVisitor.cjs");
4
5
  const require_queries_find_QueryParser = require("../queries/find/QueryParser.cjs");
5
6
  const require_utils = require("../utils.cjs");
6
7
  const require_EntityMetadata = require("../EntityMetadata.cjs");
@@ -17,6 +18,7 @@ function findIdsDataLoader(em, type, filter) {
17
18
  ...opts,
18
19
  limit: void 0
19
20
  });
21
+ if (require_queries_find_QueryVisitor.isQueryProvablyEmpty(query)) return Promise.resolve([]);
20
22
  const bindings = [];
21
23
  require_dataloaders_findDataLoader.collectValues(bindings, query);
22
24
  const prepared = {
@@ -1 +1 @@
1
- {"version":3,"file":"findIdsDataLoader.cjs","names":["getMetadata","parseFindQuery","getBatchKeyFromGenericStructure","fail","kq","keyToTaggedId","collectAndReplaceArgs","buildUnnestCte","createColumnValuesFromPrepared","queryFilterHash"],"sources":["../../src/dataloaders/findIdsDataLoader.ts"],"sourcesContent":["import {\n collectAndReplaceArgs,\n collectValues,\n createColumnValuesFromPrepared,\n getBatchKeyFromGenericStructure,\n queryFilterHash,\n} from \"src/dataloaders/findDataLoader.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport type { EntityManager, MaybeAbstractEntityConstructor } from \"src/EntityManager.ts\";\nimport { getMetadata } from \"src/EntityMetadata.ts\";\nimport { keyToTaggedId } from \"src/keys.ts\";\nimport type { FilterAndSettings } from \"src/queries/find/EntityFilter.ts\";\nimport { type ParsedFindQuery, parseFindQuery } from \"src/queries/find/QueryParser.ts\";\nimport { kq } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { fail } from \"src/utils.ts\";\n\nexport const findIdsOperation = \"find-ids\";\n\nexport function findIdsDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n): Promise<string[]> {\n const { where, ...opts } = filter;\n if (opts.limit || opts.offset) {\n throw new Error(\"Cannot use limit/offset with findIdsDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const { findSettings } = em[\"prepareFind\"](meta, findIdsOperation, query, { ...opts, limit: undefined });\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, query, bindings, findSettings };\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<typeof prepared, string[]>(\n findIdsOperation,\n batchKey,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings } = entries[0];\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`${kq(primary.alias)}.id as id`];\n query.orderBys = [{ alias: primary.alias, column: \"id\", order: \"ASC\" }];\n const rows = await em[\"executePreparedFind\"](meta, findIdsOperation, query, findSettings, false);\n return [rows.map((row: any) => keyToTaggedId(meta, row.id)!)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]) unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT _find.tag, _data.id\n // FROM _find\n // CROSS JOIN LATERAL (\n // SELECT distinct a.id as id\n // FROM author a WHERE a.first_name = _find.arg1 OR a.last_name = _find.arg2\n // ) _data\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings } = entries[0];\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n\n // We're not returning the entities, just selecting their IDs\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`${kq(primary.alias)}.id as id`];\n query.orderBys = [{ alias: primary.alias, column: \"id\", order: \"ASC\" }];\n\n const query2: ParsedFindQuery = {\n selects: [\"_find.tag as tag\", \"_data.id as id\"],\n tables: [\n { join: \"primary\", table: \"_find\", alias: \"_find\" },\n { join: \"lateral\", query, table: meta.tableName, alias: \"_data\", fromAlias: \"_f\" },\n ],\n // For each unique query, capture its filter values in `bindings` to populate the CTE _find table\n ctes: [buildUnnestCte(\"_find\", argsColumns, createColumnValuesFromPrepared(args, entries))],\n orderBys: [],\n };\n\n const rows = await em[\"executePreparedFind\"](meta, findIdsOperation, query2, findSettings, false);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results: string[][] = entries.map(() => []);\n // Then put each row's ID into the tagged query it matched\n for (const row of rows) {\n results[row.tag].push(keyToTaggedId(meta, row.id)!);\n }\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: queryFilterHash },\n )\n .load(prepared);\n}\n"],"mappings":";;;;;;;;;AAiBA,MAAa,mBAAmB;AAEhC,SAAgB,kBACd,IACA,MACA,QACmB;CACnB,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,IAAI,KAAK,SAAS,KAAK,QACrB,MAAM,IAAI,MAAM,gDAAgD;CAGlE,MAAM,OAAOA,uBAAAA,YAAY,IAAI;CAC7B,MAAM,QAAQC,iCAAAA,eAAe,MAAM,OAAO,IAAI;CAC9C,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAAC,MAAM,kBAAkB,OAAO;EAAE,GAAG;EAAM,OAAO,KAAA;CAAU,CAAC;CACvG,MAAM,WAAkB,CAAC;CACzB,mCAAA,cAAc,UAAU,KAAK;CAC7B,MAAM,WAAW;EAAE;EAAQ;EAAO;EAAU;CAAa;CACzD,MAAM,WAAWC,mCAAAA,gCAAgC,MAAM,KAAK;CAE5D,OAAO,GACJ,UACC,kBACA,UACA,OAAO,YAAY;EAIjB,IAAI,QAAQ,WAAW,GAAG;GACxB,MAAM,EAAE,OAAO,iBAAiB,QAAQ;GACxC,MAAM,UAAU,MAAM,OAAO,MAAM,MAAM,EAAE,SAAS,SAAS,KAAKC,cAAAA,KAAK,YAAY;GACnF,MAAM,UAAU,CAAC,GAAGC,6BAAAA,GAAG,QAAQ,KAAK,EAAE,UAAU;GAChD,MAAM,WAAW,CAAC;IAAE,OAAO,QAAQ;IAAO,QAAQ;IAAM,OAAO;GAAM,CAAC;GAEtE,OAAO,EAAC,MADW,GAAG,sBAAsB,CAAC,MAAM,kBAAkB,OAAO,cAAc,KAAK,EAAA,CAClF,KAAK,QAAaC,aAAAA,cAAc,MAAM,IAAI,EAAE,CAAE,CAAC;EAC9D;EAaA,MAAM,EAAE,OAAO,iBAAiB,QAAQ;EACxC,MAAM,OAAOC,mCAAAA,sBAAsB,OAAO,OAAO;EACjD,MAAM,cAAc,CAAC;GAAE,YAAY;GAAO,QAAQ;EAAM,GAAG,GAAG,KAAK,KAAK,MAAM,EAAE,MAAM,CAAC;EAGvF,MAAM,UAAU,MAAM,OAAO,MAAM,MAAM,EAAE,SAAS,SAAS,KAAKH,cAAAA,KAAK,YAAY;EACnF,MAAM,UAAU,CAAC,GAAGC,6BAAAA,GAAG,QAAQ,KAAK,EAAE,UAAU;EAChD,MAAM,WAAW,CAAC;GAAE,OAAO,QAAQ;GAAO,QAAQ;GAAM,OAAO;EAAM,CAAC;EAEtE,MAAM,SAA0B;GAC9B,SAAS,CAAC,oBAAoB,gBAAgB;GAC9C,QAAQ,CACN;IAAE,MAAM;IAAW,OAAO;IAAS,OAAO;GAAQ,GAClD;IAAE,MAAM;IAAW;IAAO,OAAO,KAAK;IAAW,OAAO;IAAS,WAAW;GAAK,CACnF;GAEA,MAAM,CAACG,uBAAAA,eAAe,SAAS,aAAaC,mCAAAA,+BAA+B,MAAM,OAAO,CAAC,CAAC;GAC1F,UAAU,CAAC;EACb;EAEA,MAAM,OAAO,MAAM,GAAG,sBAAsB,CAAC,MAAM,kBAAkB,QAAQ,cAAc,KAAK;EAGhG,MAAM,UAAsB,QAAQ,UAAU,CAAC,CAAC;EAEhD,KAAK,MAAM,OAAO,MAChB,QAAQ,IAAI,IAAI,CAAC,KAAKH,aAAAA,cAAc,MAAM,IAAI,EAAE,CAAE;EAEpD,OAAO;CACT,GAEA,EAAE,YAAYI,mCAAAA,gBAAgB,CAChC,CAAC,CACA,KAAK,QAAQ;AAClB"}
1
+ {"version":3,"file":"findIdsDataLoader.cjs","names":["getMetadata","parseFindQuery","isQueryProvablyEmpty","getBatchKeyFromGenericStructure","fail","kq","keyToTaggedId","collectAndReplaceArgs","buildUnnestCte","createColumnValuesFromPrepared","queryFilterHash"],"sources":["../../src/dataloaders/findIdsDataLoader.ts"],"sourcesContent":["import {\n collectAndReplaceArgs,\n collectValues,\n createColumnValuesFromPrepared,\n getBatchKeyFromGenericStructure,\n queryFilterHash,\n} from \"src/dataloaders/findDataLoader.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport type { EntityManager, MaybeAbstractEntityConstructor } from \"src/EntityManager.ts\";\nimport { getMetadata } from \"src/EntityMetadata.ts\";\nimport { keyToTaggedId } from \"src/keys.ts\";\nimport type { FilterAndSettings } from \"src/queries/find/EntityFilter.ts\";\nimport { type ParsedFindQuery, parseFindQuery } from \"src/queries/find/QueryParser.ts\";\nimport { isQueryProvablyEmpty } from \"src/queries/find/QueryVisitor.ts\";\nimport { kq } from \"src/queries/sql/keywords.ts\";\nimport { buildUnnestCte } from \"src/queries/unnest.ts\";\nimport { fail } from \"src/utils.ts\";\n\nexport const findIdsOperation = \"find-ids\";\n\nexport function findIdsDataLoader<T extends Entity>(\n em: EntityManager,\n type: MaybeAbstractEntityConstructor<T>,\n filter: FilterAndSettings<T>,\n): Promise<string[]> {\n const { where, ...opts } = filter;\n if (opts.limit || opts.offset) {\n throw new Error(\"Cannot use limit/offset with findIdsDataLoader\");\n }\n\n const meta = getMetadata(type);\n const query = parseFindQuery(meta, where, opts);\n const { findSettings } = em[\"prepareFind\"](meta, findIdsOperation, query, { ...opts, limit: undefined });\n if (isQueryProvablyEmpty(query)) return Promise.resolve([]);\n\n const bindings: any[] = [];\n collectValues(bindings, query);\n const prepared = { filter, query, bindings, findSettings };\n const batchKey = getBatchKeyFromGenericStructure(meta, query);\n\n return em\n .getLoader<typeof prepared, string[]>(\n findIdsOperation,\n batchKey,\n async (entries) => {\n // We're guaranteed that these queries all have the same structure\n\n // Don't bother with the CTE if there's only 1 query (or each query has exactly the same filter values)\n if (entries.length === 1) {\n const { query, findSettings } = entries[0];\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`${kq(primary.alias)}.id as id`];\n query.orderBys = [{ alias: primary.alias, column: \"id\", order: \"ASC\" }];\n const rows = await em[\"executePreparedFind\"](meta, findIdsOperation, query, findSettings, false);\n return [rows.map((row: any) => keyToTaggedId(meta, row.id)!)];\n }\n\n // WITH _find (tag, arg1, arg2) AS (\n // SELECT unnest($0::int[]) unnest($0::varchar[]), unnest($0::varchar[])\n // )\n // SELECT _find.tag, _data.id\n // FROM _find\n // CROSS JOIN LATERAL (\n // SELECT distinct a.id as id\n // FROM author a WHERE a.first_name = _find.arg1 OR a.last_name = _find.arg2\n // ) _data\n\n // Build the list of 'arg1', 'arg2', ... strings\n const { query, findSettings } = entries[0];\n const args = collectAndReplaceArgs(query, entries);\n const argsColumns = [{ columnName: \"tag\", dbType: \"int\" }, ...args.map((a) => a.column)];\n\n // We're not returning the entities, just selecting their IDs\n const primary = query.tables.find((t) => t.join === \"primary\") ?? fail(\"No primary\");\n query.selects = [`${kq(primary.alias)}.id as id`];\n query.orderBys = [{ alias: primary.alias, column: \"id\", order: \"ASC\" }];\n\n const query2: ParsedFindQuery = {\n selects: [\"_find.tag as tag\", \"_data.id as id\"],\n tables: [\n { join: \"primary\", table: \"_find\", alias: \"_find\" },\n { join: \"lateral\", query, table: meta.tableName, alias: \"_data\", fromAlias: \"_f\" },\n ],\n // For each unique query, capture its filter values in `bindings` to populate the CTE _find table\n ctes: [buildUnnestCte(\"_find\", argsColumns, createColumnValuesFromPrepared(args, entries))],\n orderBys: [],\n };\n\n const rows = await em[\"executePreparedFind\"](meta, findIdsOperation, query2, findSettings, false);\n\n // Make an empty array for each batched query, per the dataloader contract\n const results: string[][] = entries.map(() => []);\n // Then put each row's ID into the tagged query it matched\n for (const row of rows) {\n results[row.tag].push(keyToTaggedId(meta, row.id)!);\n }\n return results;\n },\n // Our filter/order tuple is a complex object, so use a stable cache key to ensure caching works.\n { cacheKeyFn: queryFilterHash },\n )\n .load(prepared);\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAa,mBAAmB;AAEhC,SAAgB,kBACd,IACA,MACA,QACmB;CACnB,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,IAAI,KAAK,SAAS,KAAK,QACrB,MAAM,IAAI,MAAM,gDAAgD;CAGlE,MAAM,OAAOA,uBAAAA,YAAY,IAAI;CAC7B,MAAM,QAAQC,iCAAAA,eAAe,MAAM,OAAO,IAAI;CAC9C,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAAC,MAAM,kBAAkB,OAAO;EAAE,GAAG;EAAM,OAAO,KAAA;CAAU,CAAC;CACvG,IAAIC,kCAAAA,qBAAqB,KAAK,GAAG,OAAO,QAAQ,QAAQ,CAAC,CAAC;CAE1D,MAAM,WAAkB,CAAC;CACzB,mCAAA,cAAc,UAAU,KAAK;CAC7B,MAAM,WAAW;EAAE;EAAQ;EAAO;EAAU;CAAa;CACzD,MAAM,WAAWC,mCAAAA,gCAAgC,MAAM,KAAK;CAE5D,OAAO,GACJ,UACC,kBACA,UACA,OAAO,YAAY;EAIjB,IAAI,QAAQ,WAAW,GAAG;GACxB,MAAM,EAAE,OAAO,iBAAiB,QAAQ;GACxC,MAAM,UAAU,MAAM,OAAO,MAAM,MAAM,EAAE,SAAS,SAAS,KAAKC,cAAAA,KAAK,YAAY;GACnF,MAAM,UAAU,CAAC,GAAGC,6BAAAA,GAAG,QAAQ,KAAK,EAAE,UAAU;GAChD,MAAM,WAAW,CAAC;IAAE,OAAO,QAAQ;IAAO,QAAQ;IAAM,OAAO;GAAM,CAAC;GAEtE,OAAO,EAAC,MADW,GAAG,sBAAsB,CAAC,MAAM,kBAAkB,OAAO,cAAc,KAAK,EAAA,CAClF,KAAK,QAAaC,aAAAA,cAAc,MAAM,IAAI,EAAE,CAAE,CAAC;EAC9D;EAaA,MAAM,EAAE,OAAO,iBAAiB,QAAQ;EACxC,MAAM,OAAOC,mCAAAA,sBAAsB,OAAO,OAAO;EACjD,MAAM,cAAc,CAAC;GAAE,YAAY;GAAO,QAAQ;EAAM,GAAG,GAAG,KAAK,KAAK,MAAM,EAAE,MAAM,CAAC;EAGvF,MAAM,UAAU,MAAM,OAAO,MAAM,MAAM,EAAE,SAAS,SAAS,KAAKH,cAAAA,KAAK,YAAY;EACnF,MAAM,UAAU,CAAC,GAAGC,6BAAAA,GAAG,QAAQ,KAAK,EAAE,UAAU;EAChD,MAAM,WAAW,CAAC;GAAE,OAAO,QAAQ;GAAO,QAAQ;GAAM,OAAO;EAAM,CAAC;EAEtE,MAAM,SAA0B;GAC9B,SAAS,CAAC,oBAAoB,gBAAgB;GAC9C,QAAQ,CACN;IAAE,MAAM;IAAW,OAAO;IAAS,OAAO;GAAQ,GAClD;IAAE,MAAM;IAAW;IAAO,OAAO,KAAK;IAAW,OAAO;IAAS,WAAW;GAAK,CACnF;GAEA,MAAM,CAACG,uBAAAA,eAAe,SAAS,aAAaC,mCAAAA,+BAA+B,MAAM,OAAO,CAAC,CAAC;GAC1F,UAAU,CAAC;EACb;EAEA,MAAM,OAAO,MAAM,GAAG,sBAAsB,CAAC,MAAM,kBAAkB,QAAQ,cAAc,KAAK;EAGhG,MAAM,UAAsB,QAAQ,UAAU,CAAC,CAAC;EAEhD,KAAK,MAAM,OAAO,MAChB,QAAQ,IAAI,IAAI,CAAC,KAAKH,aAAAA,cAAc,MAAM,IAAI,EAAE,CAAE;EAEpD,OAAO;CACT,GAEA,EAAE,YAAYI,mCAAAA,gBAAgB,CAChC,CAAC,CACA,KAAK,QAAQ;AAClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"findIdsDataLoader.d.cts","names":[],"sources":["../../src/dataloaders/findIdsDataLoader.ts"],"mappings":";;;;cAiBa;iBAEG,kBAAkB,UAAU,QAC1C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB"}
1
+ {"version":3,"file":"findIdsDataLoader.d.cts","names":[],"sources":["../../src/dataloaders/findIdsDataLoader.ts"],"mappings":";;;;cAkBa;iBAEG,kBAAkB,UAAU,QAC1C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB"}
@@ -1 +1 @@
1
- {"version":3,"file":"findIdsDataLoader.d.mts","names":[],"sources":["../../src/dataloaders/findIdsDataLoader.ts"],"mappings":";;;;cAiBa;iBAEG,kBAAkB,UAAU,QAC1C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB"}
1
+ {"version":3,"file":"findIdsDataLoader.d.mts","names":[],"sources":["../../src/dataloaders/findIdsDataLoader.ts"],"mappings":";;;;cAkBa;iBAEG,kBAAkB,UAAU,QAC1C,IAAI,eACJ,MAAM,+BAA+B,IACrC,QAAQ,kBAAkB,KACzB"}
@@ -1,5 +1,6 @@
1
1
  import { keyToTaggedId } from "../keys.js";
2
2
  import { kq } from "../queries/sql/keywords.js";
3
+ import { isQueryProvablyEmpty } from "../queries/find/QueryVisitor.js";
3
4
  import { parseFindQuery } from "../queries/find/QueryParser.js";
4
5
  import { fail } from "../utils.js";
5
6
  import { getMetadata } from "../EntityMetadata.js";
@@ -16,6 +17,7 @@ function findIdsDataLoader(em, type, filter) {
16
17
  ...opts,
17
18
  limit: void 0
18
19
  });
20
+ if (isQueryProvablyEmpty(query)) return Promise.resolve([]);
19
21
  const bindings = [];
20
22
  collectValues(bindings, query);
21
23
  const prepared = {