@rvoh/dream 2.35.0 → 2.36.1

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 (302) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/src/Dream.js +260 -55
  3. package/dist/cjs/src/bin/index.js +13 -0
  4. package/dist/cjs/src/cli/index.js +28 -17
  5. package/dist/cjs/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
  6. package/dist/cjs/src/decorators/Decorators.js +26 -4
  7. package/dist/cjs/src/decorators/field/Encrypted.js +35 -6
  8. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +6 -6
  9. package/dist/cjs/src/dream/DreamInstanceTransactionBuilder.js +63 -27
  10. package/dist/cjs/src/dream/LeftJoinLoadBuilder.js +3 -3
  11. package/dist/cjs/src/dream/LoadBuilder.js +1 -1
  12. package/dist/cjs/src/dream/Query.js +542 -167
  13. package/dist/cjs/src/dream/QueryDriver/Base.js +72 -0
  14. package/dist/cjs/src/dream/QueryDriver/Kysely.js +216 -50
  15. package/dist/cjs/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
  16. package/dist/cjs/src/dream/internal/associations/associationQuery.js +4 -0
  17. package/dist/cjs/src/dream/internal/reload.js +6 -1
  18. package/dist/cjs/src/dream/internal/saveDream.js +8 -2
  19. package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +2 -0
  20. package/dist/cjs/src/dream/internal/undestroyDream.js +1 -1
  21. package/dist/cjs/src/dream-app/index.js +38 -0
  22. package/dist/cjs/src/encrypt/InternalEncrypt.js +77 -4
  23. package/dist/cjs/src/encrypt/defaultEncryptedColumnName.js +10 -0
  24. package/dist/cjs/src/encrypt/index.js +76 -9
  25. package/dist/cjs/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
  26. package/dist/cjs/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
  27. package/dist/cjs/src/helpers/Env.js +28 -0
  28. package/dist/cjs/src/helpers/cli/generateDreamContent.js +2 -1
  29. package/dist/cjs/src/helpers/cli/generateFactoryContent.js +1 -1
  30. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +9 -9
  31. package/dist/cjs/src/helpers/cli/generateSerializerContent.js +21 -8
  32. package/dist/cjs/src/serializer/SerializerRenderer.js +65 -13
  33. package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +54 -38
  34. package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
  35. package/dist/cjs/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
  36. package/dist/esm/src/Dream.js +260 -55
  37. package/dist/esm/src/bin/index.js +13 -0
  38. package/dist/esm/src/cli/index.js +28 -17
  39. package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
  40. package/dist/esm/src/decorators/Decorators.js +26 -4
  41. package/dist/esm/src/decorators/field/Encrypted.js +35 -6
  42. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +6 -6
  43. package/dist/esm/src/dream/DreamInstanceTransactionBuilder.js +63 -27
  44. package/dist/esm/src/dream/LeftJoinLoadBuilder.js +3 -3
  45. package/dist/esm/src/dream/LoadBuilder.js +1 -1
  46. package/dist/esm/src/dream/Query.js +542 -167
  47. package/dist/esm/src/dream/QueryDriver/Base.js +72 -0
  48. package/dist/esm/src/dream/QueryDriver/Kysely.js +216 -50
  49. package/dist/esm/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
  50. package/dist/esm/src/dream/internal/associations/associationQuery.js +4 -0
  51. package/dist/esm/src/dream/internal/reload.js +6 -1
  52. package/dist/esm/src/dream/internal/saveDream.js +8 -2
  53. package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +2 -0
  54. package/dist/esm/src/dream/internal/undestroyDream.js +1 -1
  55. package/dist/esm/src/dream-app/index.js +38 -0
  56. package/dist/esm/src/encrypt/InternalEncrypt.js +77 -4
  57. package/dist/esm/src/encrypt/defaultEncryptedColumnName.js +10 -0
  58. package/dist/esm/src/encrypt/index.js +76 -9
  59. package/dist/esm/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
  60. package/dist/esm/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
  61. package/dist/esm/src/helpers/Env.js +28 -0
  62. package/dist/esm/src/helpers/cli/generateDreamContent.js +2 -1
  63. package/dist/esm/src/helpers/cli/generateFactoryContent.js +1 -1
  64. package/dist/esm/src/helpers/cli/generateMigrationContent.js +9 -9
  65. package/dist/esm/src/helpers/cli/generateSerializerContent.js +21 -8
  66. package/dist/esm/src/serializer/SerializerRenderer.js +65 -13
  67. package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +54 -38
  68. package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
  69. package/dist/esm/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
  70. package/dist/types/src/Dream.d.ts +208 -42
  71. package/dist/types/src/bin/index.d.ts +7 -1
  72. package/dist/types/src/cli/index.d.ts +13 -0
  73. package/dist/types/src/db/migration-helpers/DreamMigrationHelpers.d.ts +2 -2
  74. package/dist/types/src/decorators/Decorators.d.ts +26 -3
  75. package/dist/types/src/decorators/field/Encrypted.d.ts +6 -1
  76. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +9 -9
  77. package/dist/types/src/dream/DreamInstanceTransactionBuilder.d.ts +52 -25
  78. package/dist/types/src/dream/LeftJoinLoadBuilder.d.ts +4 -4
  79. package/dist/types/src/dream/LoadBuilder.d.ts +1 -1
  80. package/dist/types/src/dream/Query.d.ts +272 -33
  81. package/dist/types/src/dream/QueryDriver/Base.d.ts +63 -1
  82. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +85 -1
  83. package/dist/types/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.d.ts +29 -0
  84. package/dist/types/src/dream-app/index.d.ts +56 -2
  85. package/dist/types/src/encrypt/InternalEncrypt.d.ts +29 -1
  86. package/dist/types/src/encrypt/defaultEncryptedColumnName.d.ts +7 -0
  87. package/dist/types/src/encrypt/index.d.ts +75 -6
  88. package/dist/types/src/errors/associations/UnrecognizedAssociationConditionKeys.d.ts +8 -0
  89. package/dist/types/src/helpers/Env.d.ts +28 -0
  90. package/dist/types/src/package-exports/openapi.d.ts +1 -1
  91. package/dist/types/src/package-exports/types.d.ts +2 -2
  92. package/dist/types/src/serializer/SerializerRenderer.d.ts +13 -0
  93. package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +59 -55
  94. package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +40 -19
  95. package/dist/types/src/serializer/helpers/serializersForAssociatedClass.d.ts +17 -0
  96. package/dist/types/src/types/lifecycle.d.ts +6 -0
  97. package/dist/types/src/types/openapi.d.ts +52 -29
  98. package/dist/types/src/types/serializer.d.ts +43 -2
  99. package/dist/types/src/types/types/lifecycle.ts +6 -0
  100. package/dist/types/src/types/types/openapi.ts +51 -37
  101. package/dist/types/src/types/types/serializer.ts +62 -2
  102. package/dist/types/src/types/types/variadic.ts +99 -43
  103. package/dist/types/src/types/variadic.d.ts +29 -3
  104. package/docs/assets/hierarchy.js +1 -1
  105. package/docs/assets/highlight.css +4 -11
  106. package/docs/assets/navigation.js +1 -1
  107. package/docs/assets/search.js +1 -1
  108. package/docs/classes/db.DreamMigrationHelpers.html +13 -13
  109. package/docs/classes/db.KyselyQueryDriver.html +85 -46
  110. package/docs/classes/db.PostgresQueryDriver.html +86 -47
  111. package/docs/classes/db.QueryDriverBase.html +84 -45
  112. package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
  113. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  114. package/docs/classes/errors.ColumnOverflow.html +3 -3
  115. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  116. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  117. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  118. package/docs/classes/errors.DecryptionError.html +2 -2
  119. package/docs/classes/errors.DecryptionParseError.html +2 -2
  120. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  121. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  122. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  123. package/docs/classes/errors.InvalidClockTime.html +2 -2
  124. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  125. package/docs/classes/errors.InvalidDateTime.html +2 -2
  126. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  127. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  128. package/docs/classes/errors.NotNullViolation.html +3 -3
  129. package/docs/classes/errors.RecordNotFound.html +3 -3
  130. package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
  131. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
  132. package/docs/classes/errors.ValidationError.html +3 -3
  133. package/docs/classes/index.CalendarDate.html +33 -33
  134. package/docs/classes/index.ClockTime.html +32 -32
  135. package/docs/classes/index.ClockTimeTz.html +35 -35
  136. package/docs/classes/index.DateTime.html +86 -86
  137. package/docs/classes/index.Decorators.html +37 -23
  138. package/docs/classes/index.Dream.html +270 -142
  139. package/docs/classes/index.DreamApp.html +40 -10
  140. package/docs/classes/index.DreamTransaction.html +3 -3
  141. package/docs/classes/index.Env.html +16 -2
  142. package/docs/classes/index.Query.html +331 -129
  143. package/docs/classes/system.CliFileWriter.html +4 -4
  144. package/docs/classes/system.DreamBin.html +6 -2
  145. package/docs/classes/system.DreamCLI.html +7 -7
  146. package/docs/classes/system.DreamImporter.html +2 -2
  147. package/docs/classes/system.DreamLogos.html +2 -2
  148. package/docs/classes/system.DreamSerializerBuilder.html +52 -34
  149. package/docs/classes/system.ObjectSerializerBuilder.html +41 -22
  150. package/docs/classes/system.PathHelpers.html +4 -4
  151. package/docs/classes/utils.Encrypt.html +54 -10
  152. package/docs/classes/utils.Range.html +2 -2
  153. package/docs/functions/db.closeAllDbConnections.html +1 -1
  154. package/docs/functions/db.dreamDbConnections.html +1 -1
  155. package/docs/functions/db.untypedDb.html +1 -1
  156. package/docs/functions/db.validateColumn.html +1 -1
  157. package/docs/functions/db.validateTable.html +1 -1
  158. package/docs/functions/errors.pgErrorType.html +1 -1
  159. package/docs/functions/index.DreamSerializer.html +1 -1
  160. package/docs/functions/index.ObjectSerializer.html +1 -1
  161. package/docs/functions/index.ReplicaSafe.html +1 -1
  162. package/docs/functions/index.STI.html +1 -1
  163. package/docs/functions/index.SoftDelete.html +1 -1
  164. package/docs/functions/utils.camelize.html +1 -1
  165. package/docs/functions/utils.capitalize.html +1 -1
  166. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  167. package/docs/functions/utils.compact.html +1 -1
  168. package/docs/functions/utils.groupBy.html +1 -1
  169. package/docs/functions/utils.hyphenize.html +1 -1
  170. package/docs/functions/utils.intersection.html +1 -1
  171. package/docs/functions/utils.isEmpty.html +1 -1
  172. package/docs/functions/utils.normalizeUnicode.html +1 -1
  173. package/docs/functions/utils.pascalize.html +1 -1
  174. package/docs/functions/utils.percent.html +1 -1
  175. package/docs/functions/utils.range.html +1 -1
  176. package/docs/functions/utils.round.html +1 -1
  177. package/docs/functions/utils.sanitizeString.html +1 -1
  178. package/docs/functions/utils.snakeify.html +1 -1
  179. package/docs/functions/utils.sort.html +1 -1
  180. package/docs/functions/utils.sortBy.html +1 -1
  181. package/docs/functions/utils.sortObjectByKey.html +1 -1
  182. package/docs/functions/utils.sortObjectByValue.html +1 -1
  183. package/docs/functions/utils.uncapitalize.html +1 -1
  184. package/docs/functions/utils.uniq.html +1 -1
  185. package/docs/hierarchy.html +1 -1
  186. package/docs/index.html +1 -1
  187. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  188. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  189. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  190. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  191. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  192. package/docs/interfaces/types.DecoratorContext.html +2 -2
  193. package/docs/interfaces/types.DecryptCallbacks.html +6 -0
  194. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  195. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  196. package/docs/interfaces/types.DreamDbConfig.html +6 -6
  197. package/docs/interfaces/types.DurationObject.html +2 -2
  198. package/docs/interfaces/types.EncryptOptions.html +2 -2
  199. package/docs/interfaces/types.EncryptionLegacyKeyUsedEvent.html +10 -0
  200. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  201. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  202. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  203. package/docs/modules/openapi.html +1 -1
  204. package/docs/modules/types.html +1 -1
  205. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  206. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  207. package/docs/types/openapi.OpenapiFormats.html +1 -1
  208. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  209. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  210. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  211. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  212. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  213. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  214. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  215. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  216. package/docs/types/openapi.OpenapiSchemaCombinatorFields.html +4 -0
  217. package/docs/types/openapi.OpenapiSchemaCommonFields.html +5 -1
  218. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -2
  219. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -2
  220. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -2
  221. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  222. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  223. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  224. package/docs/types/openapi.OpenapiSchemaNull.html +4 -2
  225. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  226. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  227. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +5 -1
  228. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +2 -1
  229. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  230. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  231. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  232. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  233. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +1 -2
  234. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -2
  235. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -2
  236. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  237. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  238. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  239. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  240. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  241. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  242. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  243. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  244. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  245. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  246. package/docs/types/types.CalendarDateObject.html +1 -1
  247. package/docs/types/types.Camelized.html +1 -1
  248. package/docs/types/types.ClockTimeObject.html +1 -1
  249. package/docs/types/types.DbConnectionType.html +1 -1
  250. package/docs/types/types.DbTypes.html +1 -1
  251. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  252. package/docs/types/types.DreamAttributes.html +1 -1
  253. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  254. package/docs/types/types.DreamClassColumn.html +1 -1
  255. package/docs/types/types.DreamColumn.html +1 -1
  256. package/docs/types/types.DreamColumnNames.html +1 -1
  257. package/docs/types/types.DreamLogLevel.html +1 -1
  258. package/docs/types/types.DreamLogger.html +2 -2
  259. package/docs/types/types.DreamModelSerializerType.html +1 -1
  260. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  261. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  262. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  263. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  264. package/docs/types/types.DreamSerializable.html +1 -1
  265. package/docs/types/types.DreamSerializableArray.html +1 -1
  266. package/docs/types/types.DreamSerializerKey.html +1 -1
  267. package/docs/types/types.DreamSerializers.html +1 -1
  268. package/docs/types/types.DreamVirtualColumns.html +1 -1
  269. package/docs/types/types.DurationUnit.html +1 -1
  270. package/docs/types/types.EncryptAlgorithm.html +1 -1
  271. package/docs/types/types.HasManyStatement.html +1 -1
  272. package/docs/types/types.HasOneStatement.html +1 -1
  273. package/docs/types/types.Hyphenized.html +1 -1
  274. package/docs/types/types.Pascalized.html +1 -1
  275. package/docs/types/types.PrimaryKeyType.html +1 -1
  276. package/docs/types/types.RoundingPrecision.html +1 -1
  277. package/docs/types/types.SerializerCasing.html +1 -1
  278. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  279. package/docs/types/types.Snakeified.html +1 -1
  280. package/docs/types/types.StrictInterface.html +1 -1
  281. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  282. package/docs/types/types.UpdateableProperties.html +1 -1
  283. package/docs/types/types.ValidationType.html +1 -1
  284. package/docs/types/types.ViewModel.html +2 -2
  285. package/docs/types/types.ViewModelClass.html +1 -1
  286. package/docs/types/types.WeekdayName.html +1 -1
  287. package/docs/types/types.WhereStatementForDream.html +1 -1
  288. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  289. package/docs/variables/index.DreamConst.html +1 -1
  290. package/docs/variables/index.ops.html +1 -1
  291. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  292. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  293. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  294. package/docs/variables/system.primaryKeyTypes.html +1 -1
  295. package/package.json +1 -1
  296. package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +0 -37
  297. package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +0 -37
  298. package/dist/types/src/serializer/helpers/serializerForAssociatedClass.d.ts +0 -9
  299. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +0 -1
  300. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +0 -1
  301. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +0 -1
  302. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +0 -1
@@ -1,3 +1,4 @@
1
+ import * as crypto from 'crypto';
1
2
  import { SOFT_DELETE_SCOPE_NAME } from '../decorators/class/SoftDelete.js';
2
3
  import acquireStabilizedSortableBatchLocks from '../decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js';
3
4
  import { invalidateSortableRowCache } from '../decorators/field/sortable/helpers/sortableRowCache.js';
@@ -23,7 +24,9 @@ import isObject from '../helpers/isObject.js';
23
24
  import namespaceColumn from '../helpers/namespaceColumn.js';
24
25
  import protectAgainstPollutingAssignment from '../helpers/protectAgainstPollutingAssignment.js';
25
26
  import { toSafeObject } from '../helpers/toSafeObject.js';
27
+ import uniq from '../helpers/uniq.js';
26
28
  import ops from '../ops/index.js';
29
+ import assertRecognizedAssociationConditionKeys, { ASSOCIATION_CONDITION_KEYS, } from './internal/associations/assertRecognizedAssociationConditionKeys.js';
27
30
  import buildSerializerPreloadPaths from './internal/buildSerializerPreloadPaths.js';
28
31
  import computedPaginatePage from './internal/computedPaginatePage.js';
29
32
  import convertDreamClassAndAssociationNameTupleArrayToPreloadArgs from './internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js';
@@ -211,6 +214,16 @@ export default class Query {
211
214
  * The distinct column to apply to the Query
212
215
  */
213
216
  distinctColumn = null;
217
+ /**
218
+ * @internal
219
+ *
220
+ * Whether the Query is ordered by its own order statements alone. The
221
+ * driver otherwise sorts by a similarity condition's rank, and by the
222
+ * declared `order` of the associations the Query is built on or joins,
223
+ * ahead of those statements. The keyset walks and cursor pagination set
224
+ * this, since their cursors compare only the Query's order statements.
225
+ */
226
+ bypassImplicitOrder = false;
214
227
  /**
215
228
  * @internal
216
229
  *
@@ -276,6 +289,7 @@ export default class Query {
276
289
  this.distinctColumn = opts.distinctColumn || null;
277
290
  this.connectionOverride = opts.connection;
278
291
  this.shouldReallyDestroy = opts.shouldReallyDestroy || false;
292
+ this.bypassImplicitOrder = opts.bypassImplicitOrder || false;
279
293
  this.originalOpts = Object.freeze(opts);
280
294
  }
281
295
  /**
@@ -340,6 +354,7 @@ export default class Query {
340
354
  transaction: opts.transaction || this.dreamTransaction,
341
355
  connection: opts.connection || this.connectionOverride,
342
356
  shouldReallyDestroy: opts.shouldReallyDestroy !== undefined ? opts.shouldReallyDestroy : this.shouldReallyDestroy,
357
+ bypassImplicitOrder: opts.bypassImplicitOrder !== undefined ? opts.bypassImplicitOrder : this.bypassImplicitOrder,
343
358
  });
344
359
  }
345
360
  /**
@@ -438,25 +453,49 @@ export default class Query {
438
453
  *
439
454
  * IMPORTANT: `findEach` always iterates in ascending primary key order.
440
455
  * Any `order` applied to the Query is discarded; passing one has no effect
441
- * on the order records are visited in.
456
+ * on the order records are visited in. The same goes for the `order` an
457
+ * association declares, when the Query is an `associationQuery` or joins
458
+ * that association, and for the relevance ranking of a similarity condition
459
+ * (`ops.similarity`, `ops.wordSimilarity`, `ops.strictWordSimilarity`),
460
+ * which still decides which records are visited, but not in what order.
461
+ * `findEach` visits the records {@link Query.all} returns: a Query that
462
+ * joins a HasOne or HasMany association (directly, through an association
463
+ * it joins, or as the association an `associationQuery` goes through)
464
+ * returns a record once for each row the join matches, and `findEach`
465
+ * visits it once for each of those rows, the rows of one record in
466
+ * ascending order of the joined rows' primary keys. Call `distinct()` to
467
+ * visit each record once.
442
468
  *
443
469
  * This is not a limitation that can be lifted. `findEach` guarantees that
444
- * every matching record is visited exactly once, and it delivers that by
445
- * paginating on a keyset: each batch selects the next `batchSize` records
446
- * with a primary key greater than the last record of the previous batch.
447
- * That guarantee only holds over a stable, unique key. Ordering by a
448
- * mutable, non-unique column would mean a record whose sort value changed
449
- * partway through the iteration could be skipped entirely or visited twice.
450
- * ({@link Query.cursorPaginate} can honor an arbitrary order precisely
451
- * because a paginated UI tolerates that anomaly; a full traversal cannot.)
470
+ * every matching row is visited exactly once, and it delivers that by
471
+ * paginating on a keyset: each batch selects the next `batchSize` rows
472
+ * whose primary keys (the record's, then the joined rows') sort after the
473
+ * last row of the previous batch. That guarantee only holds over a stable,
474
+ * unique key. Ordering by a mutable, non-unique column would mean a record
475
+ * whose sort value changed partway through the iteration could be skipped
476
+ * entirely or visited twice. ({@link Query.cursorPaginate} can honor an
477
+ * arbitrary order precisely because a paginated UI tolerates that anomaly;
478
+ * a full traversal cannot.)
479
+ *
480
+ * A Query that keeps one record of each group (one that calls `distinct`,
481
+ * or an `associationQuery` of an association that declares `distinct`, or
482
+ * a Query that joins one) cannot be paginated that way: its own `order`, or
483
+ * the association's, chooses which record of each group it returns, and a
484
+ * batch that narrowed it to the rows after the previous batch would let it
485
+ * choose another. So `findEach` reads the keys of all of its rows before
486
+ * visiting the first, and visits those whose rows still match when their
487
+ * batch is read. It visits the rows the Query returned when the walk began,
488
+ * less any destroyed or changed so that they no longer match the Query's
489
+ * conditions, and not a row the Query returns only after the walk began,
490
+ * such as the next record of a group whose record the callback destroyed.
452
491
  *
453
492
  * If you need records processed in a particular order, load them with a
454
493
  * regular ordered query instead of `findEach`.
455
494
  *
456
- * A `limit` or `offset` on the Query cannot compose with the batch windows
457
- * — each batch re-applies the Query's conditions, so the limit would be
495
+ * A `limit` or `offset` on the Query cannot compose with the batches —
496
+ * each batch re-applies the Query's conditions, so the limit would be
458
497
  * silently replaced by `batchSize` and the offset re-applied to every
459
- * window — so a limit- or offset-carrying Query throws at runtime.
498
+ * batch — so a limit- or offset-carrying Query throws at runtime.
460
499
  *
461
500
  * @param cb - The callback to call for each found record
462
501
  * @param __namedParameters - Options for batch processing
@@ -465,33 +504,149 @@ export default class Query {
465
504
  * @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
466
505
  */
467
506
  async findEach(cb, { batchSize = Query.BATCH_SIZES.FIND_EACH } = {}) {
507
+ await this.findEachRow(this.rowKeyColumns(), cb, batchSize);
508
+ }
509
+ /**
510
+ * @internal
511
+ *
512
+ * `findEach`, visiting each record once rather than once for each row a
513
+ * HasOne or HasMany join returns it in, for the operations that act on each
514
+ * record it visits (`destroy`, `reallyDestroy`, `undestroy` and `update`
515
+ * without `lock: true`).
516
+ */
517
+ async findEachRecordOnce(cb, { batchSize = Query.BATCH_SIZES.FIND_EACH } = {}) {
518
+ await this.findEachRow([this.namespacedPrimaryKey], cb, batchSize);
519
+ }
520
+ /**
521
+ * @internal
522
+ *
523
+ * Calls `cb` with the record of each row this Query returns, telling its
524
+ * rows apart by `rowKeyColumns` (see `walkRowBatches`).
525
+ */
526
+ async findEachRow(rowKeyColumns, cb, batchSize) {
468
527
  if (this.joinLoadActivated)
469
528
  throw new LeftJoinPreloadIncompatibleWithFindEach();
470
- // a limit or offset cannot compose with the batch windows: the window
471
- // queries would re-apply it per batch — the limit silently replaced by
472
- // batchSize, the offset skipping rows inside every window
529
+ // a limit or offset cannot compose with the batches: each batch re-applies
530
+ // the Query's conditions, so the limit would be silently replaced by
531
+ // batchSize and the offset re-applied to every batch
473
532
  if (this.limitStatement || this.offsetStatement)
474
533
  throw new BatchingIncompatibleWithLimitOrOffset();
475
- let records;
476
- const query = this.order(null)
477
- .order(this.namespacedPrimaryKey)
534
+ // read as the attribute, since primaryKeyValue() reads a primary key of 0 as null
535
+ const primaryKey = this.dreamInstance['_primaryKey'];
536
+ const primaryKeyOf = (record) => record[primaryKey];
537
+ await this.walkRowBatches(rowKeyColumns, batchSize, async (batchQuery) => {
538
+ if (rowKeyColumns.length === 1) {
539
+ const records = await batchQuery.all();
540
+ return records.map(record => ({ rowKeys: [primaryKeyOf(record)], item: record }));
541
+ }
542
+ // a record carries no joined row's keys, so the batch's rows are read
543
+ // for them first, and each is paired with its record by primary key
544
+ const rows = (await this.dbDriverInstance(batchQuery).pluck(...rowKeyColumns));
545
+ const records = new Map((await batchQuery.all()).map(record => [rowKeysIdentity([primaryKeyOf(record)]), record]));
546
+ return rows.map(rowKeys => ({ rowKeys, item: records.get(rowKeysIdentity([rowKeys[0]])) }));
547
+ }, cb);
548
+ }
549
+ /**
550
+ * @internal
551
+ *
552
+ * The columns that tell apart the rows this Query returns: its primary key,
553
+ * then the primary keys of the rows HasOne and HasMany joins add (see
554
+ * `rowKeyColumnsOfJoinedRows`).
555
+ */
556
+ rowKeyColumns() {
557
+ return [this.namespacedPrimaryKey, ...this.rowKeyColumnsOfJoinedRows()];
558
+ }
559
+ /**
560
+ * @internal
561
+ *
562
+ * A HasOne or HasMany join returns a record once for each row it matches,
563
+ * and the primary keys of the joined rows, with the Query's own, tell those
564
+ * rows apart. These are those primary keys, namespaced, other than the
565
+ * Query's own. A Query that calls `distinct` returns one row for each value
566
+ * of its distinct column rather than one for each row the join matches, so
567
+ * it has none.
568
+ */
569
+ rowKeyColumnsOfJoinedRows() {
570
+ if (this.distinctColumn)
571
+ return [];
572
+ return uniq(this.dbDriverInstance().joinedRowKeyColumns()).filter(column => column !== this.namespacedPrimaryKey);
573
+ }
574
+ /**
575
+ * @internal
576
+ *
577
+ * Walks the rows this Query returns in batches, for `findEach` and
578
+ * `pluckEach`, and calls `visit` with what `readBatch` reads for each row,
579
+ * in ascending order of `rowKeyColumns`, the columns that tell its rows
580
+ * apart. `readBatch` reads a Query that returns a batch's rows, and returns
581
+ * each row's values of `rowKeyColumns` with what to visit for it, if
582
+ * anything. A row whose keys appear again in the same batch is visited once.
583
+ *
584
+ * A select that keeps one row of each group (DISTINCT ON) is walked from the
585
+ * keys of all of its rows, read before the first batch (see
586
+ * `walkRowBatchesFromKeys`); any other, by a cursor (see
587
+ * `walkRowBatchesByCursor`).
588
+ */
589
+ async walkRowBatches(rowKeyColumns, batchSize, readBatch, visit) {
590
+ const distinctOnRowKeys = await this.dbDriverInstance().pluckDistinctOnRowKeys(rowKeyColumns);
591
+ if (distinctOnRowKeys) {
592
+ await this.walkRowBatchesFromKeys(distinctOnRowKeys, rowKeyColumns, batchSize, readBatch, visit);
593
+ }
594
+ else {
595
+ await this.walkRowBatchesByCursor(rowKeyColumns, batchSize, readBatch, visit);
596
+ }
597
+ }
598
+ /**
599
+ * @internal
600
+ *
601
+ * Walks the rows this Query returns in batches of the next `batchSize` rows
602
+ * after the last row of the batch before, compared on `rowKeyColumns` by a
603
+ * where clause, which every driver applies (see `walkRowBatches`).
604
+ */
605
+ async walkRowBatchesByCursor(rowKeyColumns, batchSize, readBatch, visit) {
606
+ const rowKeyOrder = rowKeyColumns.map(column => ({
607
+ column,
608
+ direction: 'asc',
609
+ }));
610
+ const orderedQuery = this.clone({ order: null, bypassImplicitOrder: true })
611
+ .clone({ order: rowKeyOrder })
478
612
  .limit(batchSize);
479
- // the cursor is compared against a sentinel rather than tested for
480
- // truthiness, since a primary key of 0 is a legitimate cursor value that
481
- // would otherwise reset the window to the start of the set
482
- let lastId = undefined;
613
+ let lastRowKeys = undefined;
614
+ let rowCount;
483
615
  do {
484
- if (lastId === undefined)
485
- records = await query.all();
486
- else
487
- records = await query
488
- .where({ [this.dreamInstance['_primaryKey']]: ops.greaterThan(lastId) })
489
- .all();
490
- for (const record of records) {
491
- await cb(record);
492
- }
493
- lastId = records.at(-1)?.primaryKeyValue?.();
494
- } while (records.length > 0 && records.length === batchSize);
616
+ const batchQuery = lastRowKeys
617
+ ? orderedQuery.whereAny(whereRowsSortingAfter(rowKeyOrder, lastRowKeys, 0))
618
+ : orderedQuery;
619
+ const rows = await readBatch(batchQuery);
620
+ await visitWalkedRows(rows, visit);
621
+ rowCount = rows.length;
622
+ lastRowKeys = rows.at(-1)?.rowKeys;
623
+ } while (rowCount > 0 && rowCount === batchSize);
624
+ }
625
+ /**
626
+ * @internal
627
+ *
628
+ * Walks a select that keeps one row of each group (DISTINCT ON), whose rows'
629
+ * values of `rowKeyColumns` are `distinctOnRowKeys`, read before the first
630
+ * batch (see `walkRowBatches`). Such a select cannot be walked by a cursor:
631
+ * narrowing it to the rows after the cursor before it chooses each group's
632
+ * row would let it choose a row it does not return. So each batch reads the
633
+ * rows of the next of those keys that still match, by their keys alone and
634
+ * unordered, since each group's row is chosen already, and puts them back
635
+ * in the order of those keys. The walk visits the rows the select returned
636
+ * when it began, less any destroyed or changed so that they no longer match;
637
+ * a row the select returns only after the walk began is not visited.
638
+ */
639
+ async walkRowBatchesFromKeys(distinctOnRowKeys, rowKeyColumns, batchSize, readBatch, visit) {
640
+ const unorderedQuery = this.clone({ order: null });
641
+ const rowKeysList = uniq(distinctOnRowKeys, rowKeysIdentity);
642
+ const readSize = Math.min(batchSize, ROW_KEYS_PER_READ);
643
+ for (let start = 0; start < rowKeysList.length; start += readSize) {
644
+ const batchRowKeys = rowKeysList.slice(start, start + readSize);
645
+ const positions = new Map(batchRowKeys.map((rowKeys, position) => [rowKeysIdentity(rowKeys), position]));
646
+ const position = (row) => positions.get(rowKeysIdentity(row.rowKeys)) ?? 0;
647
+ const rows = await readBatch(unorderedQuery.whereAny(whereRowKeysAmong(rowKeyColumns, batchRowKeys)));
648
+ await visitWalkedRows(rows.sort((a, b) => position(a) - position(b)), visit);
649
+ }
495
650
  }
496
651
  /**
497
652
  * Load each specified association using a single SQL query.
@@ -518,7 +673,7 @@ export default class Query {
518
673
  *
519
674
  *
520
675
  * ```ts
521
- * const posts = await user.associationQuery('posts').leftJoinPreload('comments', { visibilty: 'public' }, 'replies').all()
676
+ * const posts = await user.associationQuery('posts').leftJoinPreload('comments', { and: { visibility: 'public' } }, 'replies').all()
522
677
  * console.log(posts[0].comments[0].replies[0])
523
678
  * // [Reply{id: 1}, Reply{id: 2}]
524
679
  * ```
@@ -540,7 +695,7 @@ export default class Query {
540
695
  * See {@link Query.leftJoinPreload} for preloading in a single query.
541
696
  *
542
697
  * ```ts
543
- * const user = await User.query().preload('posts', 'comments', { visibilty: 'public' }, 'replies').first()
698
+ * const user = await User.query().preload('posts', 'comments', { and: { visibility: 'public' } }, 'replies').first()
544
699
  * console.log(user.posts[0].comments[0].replies[0])
545
700
  * // [Reply{id: 1}, Reply{id: 2}]
546
701
  * ```
@@ -699,6 +854,9 @@ export default class Query {
699
854
  //
700
855
  }
701
856
  else if (isObject(nextAssociationStatement) && previousAssociationName) {
857
+ // the node this copies into also holds nested association containers, so an
858
+ // unrecognized key must be rejected here, while it is still distinguishable
859
+ assertRecognizedAssociationConditionKeys(nextAssociationStatement, ASSOCIATION_CONDITION_KEYS, previousAssociationName);
702
860
  const clonedNextAssociationStatement = cloneDeepSafe(nextAssociationStatement);
703
861
  const keys = Object.keys(clonedNextAssociationStatement);
704
862
  keys.forEach((key) => {
@@ -1364,34 +1522,25 @@ export default class Query {
1364
1522
  throw new MissingRequiredCallbackFunctionToPluckEach('pluckEach', args);
1365
1523
  if (providedOpts !== undefined && !providedOpts?.batchSize)
1366
1524
  throw new CannotPassAdditionalFieldsToPluckEachAfterCallback('pluckEach', args);
1367
- // a limit or offset cannot compose with the batch windows: the window
1368
- // queries would replace the limit with batchSize and the offset with the
1369
- // running window offset, silently ignoring both
1525
+ // a limit or offset cannot compose with the batches: each batch re-applies
1526
+ // the Query's conditions, so the limit would be silently replaced by
1527
+ // batchSize and the offset re-applied to every batch
1370
1528
  if (this.limitStatement || this.offsetStatement)
1371
1529
  throw new BatchingIncompatibleWithLimitOrOffset();
1372
1530
  const onlyColumns = args.filter((_, index) => index < providedCbIndex);
1373
1531
  const batchSize = providedOpts?.batchSize || Query.BATCH_SIZES.PLUCK_EACH_THROUGH;
1374
- let offset = 0;
1375
- let records;
1376
- do {
1377
- const onlyIncludesPrimaryKey = onlyColumns.find(column => column === this.dreamClass.primaryKey || column === this.namespacedPrimaryKey);
1378
- const columnsIncludingPrimaryKey = onlyIncludesPrimaryKey
1379
- ? onlyColumns
1380
- : [this.namespacedPrimaryKey, ...onlyColumns];
1381
- const query = this.offset(offset)
1382
- .order(null)
1383
- .order(this.namespacedPrimaryKey)
1384
- .limit(batchSize);
1385
- records = await this.dbDriverInstance(query).pluck(...columnsIncludingPrimaryKey);
1386
- // In order to batch, we need to order by primary key, so the primary key must be plucked.
1387
- // If the developer did not include the primary key in the columns to pluck, then we prepended it above
1388
- // and need to remove it from each group of plucked columns prior to passing them into the callback functions
1389
- const vals = onlyIncludesPrimaryKey ? records : records.map(valueArr => valueArr.slice(1));
1390
- for (const val of vals) {
1391
- await providedCb(...val);
1392
- }
1393
- offset += batchSize;
1394
- } while (records.length > 0 && records.length === batchSize);
1532
+ const rowKeyColumns = this.rowKeyColumns();
1533
+ // the row keys lead, so that a batch's rows can be told apart; a column
1534
+ // asked for that is one of them is read once
1535
+ const pluckedColumns = uniq([...rowKeyColumns, ...onlyColumns], column => this.namespaceColumn(column));
1536
+ const valueIndexes = onlyColumns.map(column => pluckedColumns.findIndex(plucked => this.namespaceColumn(plucked) === this.namespaceColumn(column)));
1537
+ await this.walkRowBatches(rowKeyColumns, batchSize, async (batchQuery) => {
1538
+ const rows = (await this.dbDriverInstance(batchQuery).pluck(...pluckedColumns));
1539
+ return rows.map(row => ({
1540
+ rowKeys: row.slice(0, rowKeyColumns.length),
1541
+ item: valueIndexes.map(index => row[index]),
1542
+ }));
1543
+ }, values => providedCb(...values));
1395
1544
  }
1396
1545
  /**
1397
1546
  * Retrieves an array containing all records matching the Query.
@@ -1464,6 +1613,13 @@ export default class Query {
1464
1613
  * // 2
1465
1614
  * ```
1466
1615
  *
1616
+ * Under a similarity condition (`ops.similarity`, `ops.wordSimilarity`,
1617
+ * `ops.strictWordSimilarity`), pages come best match first: records are
1618
+ * ordered by relevance ahead of the Query's `order` (or, without one, the
1619
+ * descending primary key). For best-match-first pages, use `paginate`
1620
+ * rather than {@link Query.cursorPaginate}, which does not order by
1621
+ * relevance.
1622
+ *
1467
1623
  * @param opts - Pagination options
1468
1624
  * @param opts.page - the page number that you want to fetch results for
1469
1625
  * @param opts.pageSize - the number of results per page (optional)
@@ -1506,9 +1662,47 @@ export default class Query {
1506
1662
  * provides better performance for large datasets by using cursor-based
1507
1663
  * pagination instead of offset-based pagination.
1508
1664
  *
1509
- * Default order is ascending primary key. If an order has already been
1510
- * set on the query, and it includes the primary key (e.g.: `id: 'asc'`),
1511
- * then the implicit primary key ordering will be omitted.
1665
+ * Pages follow the Query's `order`, preceded by the `order` declared on any
1666
+ * association the Query is built on or joins (so an `associationQuery` of
1667
+ * an ordered association pages in that association's order), with
1668
+ * ascending primary key as the final tiebreaker; with neither, pages run in
1669
+ * ascending primary key order. If those orders already include the primary
1670
+ * key (e.g.: `id: 'asc'`), the implicit primary key ordering is omitted.
1671
+ *
1672
+ * A Query that joins a HasOne or HasMany association (directly, through an
1673
+ * association it joins, or as the association an `associationQuery` goes
1674
+ * through) returns a record once for each row the join matches, as
1675
+ * {@link Query.all} does, and the pages return each of those rows once,
1676
+ * sorting the rows of one record by the joined rows' primary keys after the
1677
+ * primary key. The cursor of such a Query is an opaque string that names
1678
+ * its last row by primary keys, the record's and the joined rows' (readable
1679
+ * by whoever holds it; it is not encrypted), and the next page starts after
1680
+ * that row, or, if the row is gone, after the record's first row. Any other
1681
+ * Query's cursor is its last record's primary key. A record's primary key
1682
+ * is always accepted as a cursor: the next page starts after that record's
1683
+ * first row. A cursor carries no other value its row sorts by: those are
1684
+ * read from the database when the next page is.
1685
+ *
1686
+ * A Query that calls `distinct` on its primary key (as `distinct()` does,
1687
+ * e.g.: `User.innerJoin('posts').distinct()`) returns each record once, as
1688
+ * {@link Query.all} does, and so do its pages, whose cursor is the last
1689
+ * record's primary key. A Query that calls `distinct` on another column has
1690
+ * to be ordered by that column first (e.g.:
1691
+ * `Post.distinct('category').order({ category: 'asc', createdAt: 'desc' })`),
1692
+ * or the database rejects its pages. Its pages return the records
1693
+ * {@link Query.all} returns, the one its order keeps for each value of that
1694
+ * column (the newest post of each category), and its cursor is also the
1695
+ * last record's primary key. Over an association that declares `distinct`
1696
+ * and an `order` led by its distinct column (an `associationQuery` of it, or
1697
+ * a Query that joins it), the pages return the rows {@link Query.all}
1698
+ * returns: the row the association's `order` keeps for each value of its
1699
+ * distinct column.
1700
+ *
1701
+ * A similarity condition (`ops.similarity`, `ops.wordSimilarity`,
1702
+ * `ops.strictWordSimilarity`) still decides which records match, but its
1703
+ * relevance ranking does not order the pages: a cursor can only resume from
1704
+ * values it compares, and the rank is not one of them. For best-match-first
1705
+ * pages, use {@link Query.paginate}.
1512
1706
  *
1513
1707
  * ```ts
1514
1708
  * // First page (using undefined to start from beginning)
@@ -1546,9 +1740,48 @@ export default class Query {
1546
1740
  * provides better performance for large datasets by using cursor-based
1547
1741
  * pagination instead of offset-based pagination.
1548
1742
  *
1549
- * Default order is descending primary key. If an order has already been
1550
- * set on the query, and it includes the primary key (e.g.: `id: 'asc'`),
1551
- * then the implicit primary key ordering will be omitted.
1743
+ * Pages follow the Query's `order`, preceded by the `order` declared on any
1744
+ * association the Query is built on or joins (so an `associationQuery` of
1745
+ * an ordered association pages in that association's order), with
1746
+ * descending primary key as the final tiebreaker; with neither, pages run
1747
+ * in descending primary key order. If those orders already include the
1748
+ * primary key (e.g.: `id: 'asc'`), the implicit primary key ordering is
1749
+ * omitted.
1750
+ *
1751
+ * A Query that joins a HasOne or HasMany association (directly, through an
1752
+ * association it joins, or as the association an `associationQuery` goes
1753
+ * through) returns a record once for each row the join matches, as
1754
+ * {@link Query.all} does, and the pages return each of those rows once,
1755
+ * sorting the rows of one record by the joined rows' primary keys after the
1756
+ * primary key. The cursor of such a Query is an opaque string that names
1757
+ * its last row by primary keys, the record's and the joined rows' (readable
1758
+ * by whoever holds it; it is not encrypted), and the next page starts after
1759
+ * that row, or, if the row is gone, after the record's first row. Any other
1760
+ * Query's cursor is its last record's primary key. A record's primary key
1761
+ * is always accepted as a cursor: the next page starts after that record's
1762
+ * first row. A cursor carries no other value its row sorts by: those are
1763
+ * read from the database when the next page is.
1764
+ *
1765
+ * A Query that calls `distinct` on its primary key (as `distinct()` does,
1766
+ * e.g.: `User.innerJoin('posts').distinct()`) returns each record once, as
1767
+ * {@link Query.all} does, and so do its pages, whose cursor is the last
1768
+ * record's primary key. A Query that calls `distinct` on another column has
1769
+ * to be ordered by that column first (e.g.:
1770
+ * `Post.distinct('category').order({ category: 'asc', createdAt: 'desc' })`),
1771
+ * or the database rejects its pages. Its pages return the records
1772
+ * {@link Query.all} returns, the one its order keeps for each value of that
1773
+ * column (the newest post of each category), and its cursor is also the
1774
+ * last record's primary key. Over an association that declares `distinct`
1775
+ * and an `order` led by its distinct column (an `associationQuery` of it, or
1776
+ * a Query that joins it), the pages return the rows {@link Query.all}
1777
+ * returns: the row the association's `order` keeps for each value of its
1778
+ * distinct column.
1779
+ *
1780
+ * A similarity condition (`ops.similarity`, `ops.wordSimilarity`,
1781
+ * `ops.strictWordSimilarity`) still decides which records match, but its
1782
+ * relevance ranking does not order the pages: a cursor can only resume from
1783
+ * values it compares, and the rank is not one of them. For best-match-first
1784
+ * pages, use {@link Query.paginate}.
1552
1785
  *
1553
1786
  * ```ts
1554
1787
  * // First page (using undefined to start from beginning)
@@ -1583,107 +1816,123 @@ export default class Query {
1583
1816
  throw new CannotPaginateWithOffset();
1584
1817
  if (this.joinLoadActivated)
1585
1818
  throw new CannotPaginateWithLeftJoinPreload();
1586
- const orderIncludesPrimaryKey = this.orderStatements.some(orderStatement => orderStatement.column === this.dreamClass.primaryKey ||
1587
- orderStatement.column === this.namespacedPrimaryKey);
1588
- let query = orderIncludesPrimaryKey
1589
- ? this
1590
- : this.order({ [this.namespacedPrimaryKey]: 'desc' });
1819
+ // An association's declared `order` sorts ahead of the Query's own order
1820
+ // statements, so the cursor takes both, in that order, as the statements it
1821
+ // pages by. The query then orders by those statements alone: anything else
1822
+ // the driver would sort by (a similarity condition's rank) is not something
1823
+ // the cursor compares, and would strand records between pages. A column
1824
+ // listed twice sorts only by its first appearance, so only that one is
1825
+ // kept; a later duplicate would contradict it in the cursor's comparison.
1826
+ const cursorOrderStatements = uniq([...this.dbDriverInstance().associationOrderStatements(), ...this.orderStatements], orderStatement => this.namespaceColumn(orderStatement.column));
1827
+ const primaryKeyStatement = cursorOrderStatements.find(orderStatement => orderStatement.column === this.dreamClass.primaryKey ||
1828
+ orderStatement.column === this.namespacedPrimaryKey) ?? { column: this.namespacedPrimaryKey, direction: 'desc' };
1829
+ // The primary keys of the rows a HasOne or HasMany join adds, which tell
1830
+ // apart the rows it returns a record in, follow the Query's own primary key
1831
+ // as the last statements the cursor pages by, those not already ordered
1832
+ // by, so that the rows a record is returned for sort the same way on every
1833
+ // page.
1834
+ const joinedRowKeyColumns = this.rowKeyColumnsOfJoinedRows();
1835
+ const orderedColumns = new Set([...cursorOrderStatements, primaryKeyStatement].map(orderStatement => this.namespaceColumn(orderStatement.column)));
1836
+ const orderStatements = [
1837
+ ...cursorOrderStatements,
1838
+ ...(cursorOrderStatements.includes(primaryKeyStatement) ? [] : [primaryKeyStatement]),
1839
+ ...joinedRowKeyColumns
1840
+ .filter(column => !orderedColumns.has(column))
1841
+ .map(column => ({ column, direction: primaryKeyStatement.direction })),
1842
+ ];
1843
+ const primaryKeyIndex = orderStatements.indexOf(primaryKeyStatement);
1844
+ const rowKeyIndexes = [
1845
+ primaryKeyIndex,
1846
+ ...joinedRowKeyColumns.map(column => orderStatements.findIndex(orderStatement => this.namespaceColumn(orderStatement.column) === column)),
1847
+ ];
1848
+ const rowKeyColumns = rowKeyIndexes.map(index => orderStatements[index].column);
1849
+ const orderedQuery = this.clone({ order: null, bypassImplicitOrder: true }).clone({
1850
+ order: orderStatements,
1851
+ });
1852
+ const pageQuery = orderedQuery.limit(pageSize);
1853
+ let rowsSortingAfter = null;
1591
1854
  if (options.cursor) {
1592
- const orderStatements = query.orderStatements;
1593
- if (orderStatements.length === 1) {
1594
- /**
1595
- * Since we add a primary key above if it wasn't already included, we know
1596
- * that if there is only one order statement, then it must be ordering on
1597
- * the primary key
1598
- */
1599
- const orderStatement = orderStatements[0];
1600
- switch (orderStatement.direction) {
1601
- case 'asc':
1602
- query = query.where({
1603
- [orderStatement.column]: ops.greaterThan(options.cursor),
1604
- });
1605
- break;
1606
- case 'desc':
1607
- query = query.where({
1608
- [orderStatement.column]: ops.lessThan(options.cursor),
1609
- });
1610
- break;
1611
- }
1612
- }
1613
- else {
1614
- const endOfPreviousPageComparisonValues = (await query
1615
- .removeDefaultScopeExceptOnAssociations(SOFT_DELETE_SCOPE_NAME)
1616
- .where({ [this.namespacedPrimaryKey]: options.cursor })
1617
- .limit(1)
1618
- .order(null)
1619
- .pluck(...orderStatements.map(orderStatement => orderStatement.column)))[0];
1620
- if (endOfPreviousPageComparisonValues) {
1621
- const whereAnyMaybeEqualArray = [];
1622
- for (let index = 0; index < endOfPreviousPageComparisonValues.length; index++) {
1623
- /**
1624
- * This nested loop enables us to give priority to the left-most order clause,
1625
- * then the left-most and next left-most, etc., replicating what ordering on
1626
- * multiple clauses does:
1627
- *
1628
- * const results = await Pet.query()
1629
- * .leftJoin('user')
1630
- * // The primary key is implicitly added if not explicitly present, so these two
1631
- * // order clauses appear in the query with an additional primary key clause.
1632
- * // This supports pagination when the earlier order clauses match multiple records.
1633
- * .order({ 'pets.name': 'asc', 'user.id': 'asc' })
1634
- * .cursorPaginate({ pageSize: 2, cursor: undefined })
1635
- *
1636
- * Results in:
1637
- *
1638
- * SELECT
1639
- * "pets".*
1640
- * FROM
1641
- * "pets"
1642
- * LEFT JOIN "users" AS "user" ON "pets"."user_id" = "user"."id"
1643
- * AND "user"."deleted_at" IS NULL
1644
- * WHERE ("pets"."deleted_at" IS NULL
1645
- * AND (
1646
- * "pets"."name" > $1
1647
- * OR (
1648
- * "pets"."name" = $2 AND "user"."id" > $3
1649
- * ) OR (
1650
- * "pets"."name" = $4 AND "user"."id" = $5 AND "pets"."id" < $6
1651
- * )))
1652
- * ORDER BY
1653
- * "pets"."name" ASC nulls FIRST,
1654
- * "user"."id" ASC nulls FIRST,
1655
- * "pets"."id" DESC nulls LAST
1656
- * LIMIT $7
1657
- */
1658
- const whereAnyMaybeEqual = {};
1659
- whereAnyMaybeEqualArray.push(whereAnyMaybeEqual);
1660
- for (let nestedIndex = 0; nestedIndex <= index; nestedIndex++) {
1661
- const valueToCompare = endOfPreviousPageComparisonValues[nestedIndex];
1662
- const orderStatement = orderStatements[nestedIndex];
1663
- if (nestedIndex < index) {
1664
- whereAnyMaybeEqual[orderStatement.column] = valueToCompare;
1665
- }
1666
- else if (nestedIndex === index) {
1667
- switch (orderStatement.direction) {
1668
- case 'asc':
1669
- whereAnyMaybeEqual[orderStatement.column] = ops.greaterThan(valueToCompare);
1670
- break;
1671
- case 'desc':
1672
- whereAnyMaybeEqual[orderStatement.column] = ops.lessThan(valueToCompare);
1673
- break;
1674
- }
1675
- }
1676
- }
1677
- }
1678
- query = query.whereAny(whereAnyMaybeEqualArray);
1679
- }
1855
+ const boundaryValues = await this.cursorBoundaryValues(orderedQuery, {
1856
+ cursor: options.cursor,
1857
+ orderStatements,
1858
+ rowKeyColumns,
1859
+ });
1860
+ if (boundaryValues) {
1861
+ rowsSortingAfter = whereRowsSortingAfter(orderStatements, boundaryValues, primaryKeyIndex);
1680
1862
  }
1681
1863
  }
1682
- const results = await query.limit(pageSize).all();
1683
- return {
1684
- cursor: (results.length === pageSize && results.at(-1)?.primaryKeyValue().toString()) || null,
1685
- results,
1686
- };
1864
+ // A select that keeps one row of each group (as a Query that calls
1865
+ // `distinct`, or an association that declares `distinct`, produces)
1866
+ // chooses that row from the rows it reads, so it is not narrowed to the
1867
+ // rows sorting after the cursor, which would let it choose a row it does
1868
+ // not return: the driver reads its page, choosing each group's row before
1869
+ // narrowing. Any other select is narrowed by its own conditions.
1870
+ const distinctOnPageRows = await pageQuery.dbDriverInstance().pluckDistinctOnPage(rowsSortingAfter);
1871
+ const query = rowsSortingAfter ? pageQuery.whereAny(rowsSortingAfter) : pageQuery;
1872
+ // Otherwise, without a HasOne or HasMany join, a record is returned once,
1873
+ // so its primary key identifies the row the next page starts after.
1874
+ if (!distinctOnPageRows && !joinedRowKeyColumns.length) {
1875
+ const results = await query.all();
1876
+ return {
1877
+ cursor: (results.length === pageSize && results.at(-1)?.primaryKeyValue().toString()) || null,
1878
+ results,
1879
+ };
1880
+ }
1881
+ // The page's rows are read first, and the records then by the keys of
1882
+ // those rows, so the records returned are the rows the page holds. A HasOne
1883
+ // or HasMany join can return a record for several rows, so the cursor of
1884
+ // such a Query names the last row by its keys: the record's primary key and
1885
+ // the joined rows' primary keys. Any other Query's cursor is the last row's
1886
+ // primary key.
1887
+ const pageRows = distinctOnPageRows ??
1888
+ (await query.pluck(...orderStatements.map(orderStatement => orderStatement.column)));
1889
+ if (!pageRows.length)
1890
+ return { cursor: null, results: [] };
1891
+ const pageRowKeys = pageRows.map(row => rowKeyIndexes.map(index => row[index]));
1892
+ const results = await orderedQuery.whereAny(whereRowKeysAmong(rowKeyColumns, pageRowKeys)).all();
1893
+ const lastRowKeys = pageRowKeys.at(-1);
1894
+ let cursor = null;
1895
+ if (pageRows.length === pageSize) {
1896
+ cursor = joinedRowKeyColumns.length
1897
+ ? encodeRowKeysCursor({ tableName: this.tableName, rowKeyColumns, rowKeys: lastRowKeys })
1898
+ : String(lastRowKeys[0]);
1899
+ }
1900
+ return { cursor, results };
1901
+ }
1902
+ /**
1903
+ * @internal
1904
+ *
1905
+ * The values the row a cursor names sorts by now, in the order of
1906
+ * `orderStatements`, or undefined when no such row is found.
1907
+ *
1908
+ * Unless the primary key is the only order statement, the values are read
1909
+ * from the database, a soft-deleted record included, so that a page can
1910
+ * follow a record removed since the cursor was issued. A cursor that names its row by keys this Query's rows carry is
1911
+ * resumed from that row. Any other cursor (a primary key, as every cursor
1912
+ * issued before 2.36.0 is, or a cursor from a Query that joins other
1913
+ * associations), and a cursor whose row is gone, is resumed from the first
1914
+ * row of the record whose primary key it is or carries.
1915
+ */
1916
+ async cursorBoundaryValues(orderedQuery, { cursor, orderStatements, rowKeyColumns, }) {
1917
+ const { primaryKey, rowKeys } = decodeCursorRowKeys(cursor, { tableName: this.tableName, rowKeyColumns });
1918
+ // the only order statement is the primary key, so the cursor is the value
1919
+ if (orderStatements.length === 1)
1920
+ return [primaryKey];
1921
+ const boundaryQuery = orderedQuery
1922
+ .removeDefaultScopeExceptOnAssociations(SOFT_DELETE_SCOPE_NAME)
1923
+ .limit(1);
1924
+ const orderColumns = orderStatements.map(orderStatement => orderStatement.column);
1925
+ if (rowKeys) {
1926
+ const [boundaryRow] = (await boundaryQuery
1927
+ .where(Object.fromEntries(rowKeyColumns.map((column, index) => [column, rowKeys[index]])))
1928
+ .pluck(...orderColumns));
1929
+ if (boundaryRow)
1930
+ return boundaryRow;
1931
+ }
1932
+ const [firstRowOfRecord] = (await boundaryQuery
1933
+ .where({ [this.namespacedPrimaryKey]: primaryKey })
1934
+ .pluck(...orderColumns));
1935
+ return firstRowOfRecord;
1687
1936
  }
1688
1937
  /**
1689
1938
  * Forces use of a database connection (e.g. 'primary') during the query.
@@ -1945,7 +2194,7 @@ export default class Query {
1945
2194
  if (lock) {
1946
2195
  return await this.lockedDestroy(options, resolvedBatchSize);
1947
2196
  }
1948
- await this.findEach(async (result) => {
2197
+ await this.findEachRecordOnce(async (result) => {
1949
2198
  const subquery = this.dreamTransaction
1950
2199
  ? result.txn(this.dreamTransaction)
1951
2200
  : result;
@@ -2067,7 +2316,10 @@ export default class Query {
2067
2316
  if (this.limitStatement || this.offsetStatement)
2068
2317
  throw new BatchingIncompatibleWithLimitOrOffset();
2069
2318
  const primaryKey = this.dreamInstance['_primaryKey'];
2070
- const orderedQuery = this.order(null)
2319
+ // ordered by primary key alone, so that each window holds the next
2320
+ // batchSize records after the cursor
2321
+ const orderedQuery = this.clone({ bypassImplicitOrder: true })
2322
+ .order(null)
2071
2323
  .order(this.namespacedPrimaryKey)
2072
2324
  .limit(batchSize);
2073
2325
  let counter = 0;
@@ -2204,7 +2456,7 @@ export default class Query {
2204
2456
  if (!this.dreamClass['softDelete'])
2205
2457
  throw new CannotCallUndestroyOnANonSoftDeleteModel(this.dreamClass);
2206
2458
  let counter = 0;
2207
- await this.removeDefaultScope(SOFT_DELETE_SCOPE_NAME).findEach(async (result) => {
2459
+ await this.removeDefaultScope(SOFT_DELETE_SCOPE_NAME).findEachRecordOnce(async (result) => {
2208
2460
  const subquery = this.dreamTransaction
2209
2461
  ? result.txn(this.dreamTransaction)
2210
2462
  : result;
@@ -2286,7 +2538,7 @@ export default class Query {
2286
2538
  if (!cb && skipHooks)
2287
2539
  return await this.updateWithoutCallingModelHooks(attributesOrCb);
2288
2540
  let counter = 0;
2289
- await this.findEach(async (result) => {
2541
+ await this.findEachRecordOnce(async (result) => {
2290
2542
  // as on the locked path, the callback receives a clone, so only the
2291
2543
  // attributes it returns are written
2292
2544
  const attributes = cb
@@ -2374,3 +2626,126 @@ export default class Query {
2374
2626
  return query;
2375
2627
  }
2376
2628
  }
2629
+ /**
2630
+ * Calls `visit` with the item of each of `rows` that has one, in the order of
2631
+ * `rows`, once for each row's keys.
2632
+ */
2633
+ async function visitWalkedRows(rows, visit) {
2634
+ const visitedRowKeys = new Set();
2635
+ for (const { rowKeys, item } of rows) {
2636
+ const rowKeysId = rowKeysIdentity(rowKeys);
2637
+ if (item === undefined || visitedRowKeys.has(rowKeysId))
2638
+ continue;
2639
+ visitedRowKeys.add(rowKeysId);
2640
+ await visit(item);
2641
+ }
2642
+ }
2643
+ /**
2644
+ * The most rows a batch of `walkRowBatchesFromKeys` reads by their keys in one
2645
+ * statement, so that the keys it binds stay well under Postgres's limit of
2646
+ * 65,535 parameters in a statement, alongside those the Query binds.
2647
+ */
2648
+ const ROW_KEYS_PER_READ = 1000;
2649
+ /**
2650
+ * The where clauses, any one of which matches a row whose values of
2651
+ * `rowKeyColumns` are one of `rowKeysList`.
2652
+ */
2653
+ function whereRowKeysAmong(rowKeyColumns, rowKeysList) {
2654
+ if (rowKeyColumns.length === 1)
2655
+ return [{ [rowKeyColumns[0]]: rowKeysList.map(rowKeys => rowKeys[0]) }];
2656
+ return rowKeysList.map(rowKeys => Object.fromEntries(rowKeyColumns.map((column, index) => [column, rowKeys[index]])));
2657
+ }
2658
+ /**
2659
+ * A string that two rows' keys share only when they are equal, whatever type
2660
+ * the driver returned each key as.
2661
+ */
2662
+ function rowKeysIdentity(rowKeys) {
2663
+ return JSON.stringify(rowKeys.map(value => value === null || value === undefined ? null : String(value)));
2664
+ }
2665
+ function whereRowsSortingAfter(orderStatements, boundaryValues, primaryKeyIndex) {
2666
+ const whereStatements = [];
2667
+ for (let index = 0; index < orderStatements.length; index++) {
2668
+ const equalityPrefix = {};
2669
+ for (let nestedIndex = 0; nestedIndex < index; nestedIndex++) {
2670
+ equalityPrefix[orderStatements[nestedIndex].column] = boundaryValues[nestedIndex];
2671
+ }
2672
+ const valueToCompare = boundaryValues[index];
2673
+ const orderStatement = orderStatements[index];
2674
+ const comparisonsSortingAfter = [];
2675
+ switch (orderStatement.direction) {
2676
+ case 'asc':
2677
+ comparisonsSortingAfter.push(valueToCompare === null ? ops.not.equal(null) : ops.greaterThan(valueToCompare));
2678
+ break;
2679
+ case 'desc':
2680
+ if (valueToCompare !== null)
2681
+ comparisonsSortingAfter.push(ops.lessThan(valueToCompare));
2682
+ if (valueToCompare !== null && index !== primaryKeyIndex)
2683
+ comparisonsSortingAfter.push(null);
2684
+ break;
2685
+ }
2686
+ for (const comparison of comparisonsSortingAfter) {
2687
+ whereStatements.push({ ...equalityPrefix, [orderStatement.column]: comparison });
2688
+ }
2689
+ }
2690
+ return whereStatements;
2691
+ }
2692
+ /**
2693
+ * Prefix of a cursor that names its row by keys: the record's primary key,
2694
+ * then the primary key of each row a HasOne or HasMany join matched to it. A
2695
+ * primary key (an integer or a UUID) never contains a `.`, so a cursor that is
2696
+ * a primary key is never read as one of these.
2697
+ */
2698
+ const ROW_KEYS_CURSOR_PREFIX = 'v2.';
2699
+ /**
2700
+ * A short digest of the table and columns a cursor's keys were read from, so
2701
+ * that a cursor passed to a Query that reads other columns resumes from its
2702
+ * primary key rather than matching its keys against the wrong columns.
2703
+ */
2704
+ function cursorSignature(tableAndColumns) {
2705
+ return crypto.createHash('sha256').update(JSON.stringify(tableAndColumns)).digest('base64url').slice(0, 12);
2706
+ }
2707
+ function encodeRowKeysCursor({ tableName, rowKeyColumns, rowKeys, }) {
2708
+ const payload = JSON.stringify({ s: cursorSignature([tableName, rowKeyColumns]), k: rowKeys });
2709
+ return ROW_KEYS_CURSOR_PREFIX + Buffer.from(payload, 'utf8').toString('base64url');
2710
+ }
2711
+ /**
2712
+ * The primary key a cursor is or carries, and the keys of the row it names
2713
+ * when it names one of the Query's rows by `rowKeyColumns` (otherwise null).
2714
+ * A cursor that cannot be read as one Dream issues is taken to be a primary
2715
+ * key.
2716
+ */
2717
+ function decodeCursorRowKeys(cursor, { tableName, rowKeyColumns }) {
2718
+ const rowKeysPayload = cursorPayload(cursor, ROW_KEYS_CURSOR_PREFIX);
2719
+ const keys = rowKeysPayload?.k;
2720
+ if (rowKeysPayload && Array.isArray(keys) && isCursorPrimaryKey(keys[0])) {
2721
+ const namesQueryRow = rowKeysPayload.s === cursorSignature([tableName, rowKeyColumns]) &&
2722
+ keys.length === rowKeyColumns.length &&
2723
+ keys.every(isCursorRowKey);
2724
+ return { primaryKey: keys[0], rowKeys: namesQueryRow ? keys : null };
2725
+ }
2726
+ return { primaryKey: cursor, rowKeys: null };
2727
+ }
2728
+ /**
2729
+ * The object a cursor that starts with `prefix` encodes, or null when the
2730
+ * cursor does not start with it or cannot be read.
2731
+ */
2732
+ function cursorPayload(cursor, prefix) {
2733
+ if (!cursor.startsWith(prefix))
2734
+ return null;
2735
+ try {
2736
+ const payload = JSON.parse(Buffer.from(cursor.slice(prefix.length), 'base64url').toString('utf8'));
2737
+ return isObject(payload) ? payload : null;
2738
+ }
2739
+ catch {
2740
+ return null;
2741
+ }
2742
+ }
2743
+ function isCursorPrimaryKey(value) {
2744
+ return typeof value === 'string' || typeof value === 'number';
2745
+ }
2746
+ /**
2747
+ * A joined row's key is NULL in a row a LEFT JOIN matched to no row.
2748
+ */
2749
+ function isCursorRowKey(value) {
2750
+ return value === null || isCursorPrimaryKey(value);
2751
+ }