@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.
- package/README.md +2 -2
- package/dist/cjs/src/Dream.js +260 -55
- package/dist/cjs/src/bin/index.js +13 -0
- package/dist/cjs/src/cli/index.js +28 -17
- package/dist/cjs/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
- package/dist/cjs/src/decorators/Decorators.js +26 -4
- package/dist/cjs/src/decorators/field/Encrypted.js +35 -6
- package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +6 -6
- package/dist/cjs/src/dream/DreamInstanceTransactionBuilder.js +63 -27
- package/dist/cjs/src/dream/LeftJoinLoadBuilder.js +3 -3
- package/dist/cjs/src/dream/LoadBuilder.js +1 -1
- package/dist/cjs/src/dream/Query.js +542 -167
- package/dist/cjs/src/dream/QueryDriver/Base.js +72 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +216 -50
- package/dist/cjs/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
- package/dist/cjs/src/dream/internal/associations/associationQuery.js +4 -0
- package/dist/cjs/src/dream/internal/reload.js +6 -1
- package/dist/cjs/src/dream/internal/saveDream.js +8 -2
- package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +2 -0
- package/dist/cjs/src/dream/internal/undestroyDream.js +1 -1
- package/dist/cjs/src/dream-app/index.js +38 -0
- package/dist/cjs/src/encrypt/InternalEncrypt.js +77 -4
- package/dist/cjs/src/encrypt/defaultEncryptedColumnName.js +10 -0
- package/dist/cjs/src/encrypt/index.js +76 -9
- package/dist/cjs/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
- package/dist/cjs/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
- package/dist/cjs/src/helpers/Env.js +28 -0
- package/dist/cjs/src/helpers/cli/generateDreamContent.js +2 -1
- package/dist/cjs/src/helpers/cli/generateFactoryContent.js +1 -1
- package/dist/cjs/src/helpers/cli/generateMigrationContent.js +9 -9
- package/dist/cjs/src/helpers/cli/generateSerializerContent.js +21 -8
- package/dist/cjs/src/serializer/SerializerRenderer.js +65 -13
- package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +54 -38
- package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
- package/dist/cjs/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
- package/dist/esm/src/Dream.js +260 -55
- package/dist/esm/src/bin/index.js +13 -0
- package/dist/esm/src/cli/index.js +28 -17
- package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
- package/dist/esm/src/decorators/Decorators.js +26 -4
- package/dist/esm/src/decorators/field/Encrypted.js +35 -6
- package/dist/esm/src/dream/DreamClassTransactionBuilder.js +6 -6
- package/dist/esm/src/dream/DreamInstanceTransactionBuilder.js +63 -27
- package/dist/esm/src/dream/LeftJoinLoadBuilder.js +3 -3
- package/dist/esm/src/dream/LoadBuilder.js +1 -1
- package/dist/esm/src/dream/Query.js +542 -167
- package/dist/esm/src/dream/QueryDriver/Base.js +72 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +216 -50
- package/dist/esm/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
- package/dist/esm/src/dream/internal/associations/associationQuery.js +4 -0
- package/dist/esm/src/dream/internal/reload.js +6 -1
- package/dist/esm/src/dream/internal/saveDream.js +8 -2
- package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +2 -0
- package/dist/esm/src/dream/internal/undestroyDream.js +1 -1
- package/dist/esm/src/dream-app/index.js +38 -0
- package/dist/esm/src/encrypt/InternalEncrypt.js +77 -4
- package/dist/esm/src/encrypt/defaultEncryptedColumnName.js +10 -0
- package/dist/esm/src/encrypt/index.js +76 -9
- package/dist/esm/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
- package/dist/esm/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
- package/dist/esm/src/helpers/Env.js +28 -0
- package/dist/esm/src/helpers/cli/generateDreamContent.js +2 -1
- package/dist/esm/src/helpers/cli/generateFactoryContent.js +1 -1
- package/dist/esm/src/helpers/cli/generateMigrationContent.js +9 -9
- package/dist/esm/src/helpers/cli/generateSerializerContent.js +21 -8
- package/dist/esm/src/serializer/SerializerRenderer.js +65 -13
- package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +54 -38
- package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
- package/dist/esm/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
- package/dist/types/src/Dream.d.ts +208 -42
- package/dist/types/src/bin/index.d.ts +7 -1
- package/dist/types/src/cli/index.d.ts +13 -0
- package/dist/types/src/db/migration-helpers/DreamMigrationHelpers.d.ts +2 -2
- package/dist/types/src/decorators/Decorators.d.ts +26 -3
- package/dist/types/src/decorators/field/Encrypted.d.ts +6 -1
- package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +9 -9
- package/dist/types/src/dream/DreamInstanceTransactionBuilder.d.ts +52 -25
- package/dist/types/src/dream/LeftJoinLoadBuilder.d.ts +4 -4
- package/dist/types/src/dream/LoadBuilder.d.ts +1 -1
- package/dist/types/src/dream/Query.d.ts +272 -33
- package/dist/types/src/dream/QueryDriver/Base.d.ts +63 -1
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +85 -1
- package/dist/types/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.d.ts +29 -0
- package/dist/types/src/dream-app/index.d.ts +56 -2
- package/dist/types/src/encrypt/InternalEncrypt.d.ts +29 -1
- package/dist/types/src/encrypt/defaultEncryptedColumnName.d.ts +7 -0
- package/dist/types/src/encrypt/index.d.ts +75 -6
- package/dist/types/src/errors/associations/UnrecognizedAssociationConditionKeys.d.ts +8 -0
- package/dist/types/src/helpers/Env.d.ts +28 -0
- package/dist/types/src/package-exports/openapi.d.ts +1 -1
- package/dist/types/src/package-exports/types.d.ts +2 -2
- package/dist/types/src/serializer/SerializerRenderer.d.ts +13 -0
- package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +59 -55
- package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +40 -19
- package/dist/types/src/serializer/helpers/serializersForAssociatedClass.d.ts +17 -0
- package/dist/types/src/types/lifecycle.d.ts +6 -0
- package/dist/types/src/types/openapi.d.ts +52 -29
- package/dist/types/src/types/serializer.d.ts +43 -2
- package/dist/types/src/types/types/lifecycle.ts +6 -0
- package/dist/types/src/types/types/openapi.ts +51 -37
- package/dist/types/src/types/types/serializer.ts +62 -2
- package/dist/types/src/types/types/variadic.ts +99 -43
- package/dist/types/src/types/variadic.d.ts +29 -3
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/highlight.css +4 -11
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +13 -13
- package/docs/classes/db.KyselyQueryDriver.html +85 -46
- package/docs/classes/db.PostgresQueryDriver.html +86 -47
- package/docs/classes/db.QueryDriverBase.html +84 -45
- package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
- package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +37 -23
- package/docs/classes/index.Dream.html +270 -142
- package/docs/classes/index.DreamApp.html +40 -10
- package/docs/classes/index.DreamTransaction.html +3 -3
- package/docs/classes/index.Env.html +16 -2
- package/docs/classes/index.Query.html +331 -129
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +6 -2
- package/docs/classes/system.DreamCLI.html +7 -7
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +52 -34
- package/docs/classes/system.ObjectSerializerBuilder.html +41 -22
- package/docs/classes/system.PathHelpers.html +4 -4
- package/docs/classes/utils.Encrypt.html +54 -10
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +1 -1
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +1 -1
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/index.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DecryptCallbacks.html +6 -0
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +6 -6
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.EncryptionLegacyKeyUsedEvent.html +10 -0
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/modules/openapi.html +1 -1
- package/docs/modules/types.html +1 -1
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCombinatorFields.html +4 -0
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +5 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +4 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +5 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +2 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
- package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +0 -37
- package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +0 -37
- package/dist/types/src/serializer/helpers/serializerForAssociatedClass.d.ts +0 -9
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +0 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +0 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +0 -1
- 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
|
|
445
|
-
* paginating on a keyset: each batch selects the next `batchSize`
|
|
446
|
-
*
|
|
447
|
-
* That guarantee only holds over a stable,
|
|
448
|
-
* mutable, non-unique column would mean a record
|
|
449
|
-
* partway through the iteration could be skipped
|
|
450
|
-
* ({@link Query.cursorPaginate} can honor an
|
|
451
|
-
* because a paginated UI tolerates that anomaly;
|
|
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
|
|
457
|
-
*
|
|
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
|
-
*
|
|
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
|
|
471
|
-
//
|
|
472
|
-
// batchSize
|
|
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
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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', {
|
|
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', {
|
|
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
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
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
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
.
|
|
1383
|
-
.
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
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
|
-
*
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
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
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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.
|
|
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
|
-
|
|
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).
|
|
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.
|
|
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
|
+
}
|