ultimabase 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +4 -2
- package/dist/access.d.ts +62 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +258 -0
- package/dist/adapter-errors.d.ts +43 -0
- package/dist/adapter-errors.d.ts.map +1 -0
- package/dist/adapter-errors.js +72 -0
- package/dist/adapter-json.d.ts +20 -0
- package/dist/adapter-json.d.ts.map +1 -0
- package/dist/adapter-json.js +2 -0
- package/dist/adapter-migrations.d.ts +90 -0
- package/dist/adapter-migrations.d.ts.map +1 -0
- package/dist/adapter-migrations.js +1 -0
- package/dist/adapter.d.ts +333 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +5 -0
- package/dist/aggregate.d.ts +31 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +99 -0
- package/dist/app-internals.d.ts +10 -0
- package/dist/app-internals.d.ts.map +1 -0
- package/dist/app-internals.js +12 -0
- package/dist/app-options.d.ts +22 -0
- package/dist/app-options.d.ts.map +1 -0
- package/dist/app-options.js +3 -0
- package/dist/cascade.d.ts +47 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +209 -0
- package/dist/cli-config.d.ts +6 -0
- package/dist/cli-config.d.ts.map +1 -0
- package/dist/cli-config.js +45 -0
- package/dist/cli-deploy.d.ts +14 -0
- package/dist/cli-deploy.d.ts.map +1 -0
- package/dist/cli-deploy.js +193 -0
- package/dist/cli-main.d.ts +16 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +151 -0
- package/dist/cli-migrate.d.ts +20 -0
- package/dist/cli-migrate.d.ts.map +1 -0
- package/dist/cli-migrate.js +105 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/complete.d.ts +13 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +60 -0
- package/dist/console-logger.d.ts +3 -0
- package/dist/console-logger.d.ts.map +1 -0
- package/dist/console-logger.js +62 -0
- package/dist/create-many.d.ts +7 -0
- package/dist/create-many.d.ts.map +1 -0
- package/dist/create-many.js +88 -0
- package/dist/decimal.d.ts +23 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +50 -0
- package/dist/define-app.d.ts +59 -0
- package/dist/define-app.d.ts.map +1 -0
- package/dist/define-app.js +5 -0
- package/dist/define-enum.d.ts +6 -0
- package/dist/define-enum.d.ts.map +1 -0
- package/dist/define-enum.js +18 -0
- package/dist/define-function.d.ts +25 -0
- package/dist/define-function.d.ts.map +1 -0
- package/dist/define-function.js +111 -0
- package/dist/define-model.d.ts +48 -0
- package/dist/define-model.d.ts.map +1 -0
- package/dist/define-model.js +85 -0
- package/dist/definition.d.ts +179 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +165 -0
- package/dist/delete-many.d.ts +13 -0
- package/dist/delete-many.d.ts.map +1 -0
- package/dist/delete-many.js +58 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +174 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +43 -0
- package/dist/exact.d.ts +20 -0
- package/dist/exact.d.ts.map +1 -0
- package/dist/exact.js +1 -0
- package/dist/feed.d.ts +6 -0
- package/dist/feed.d.ts.map +1 -0
- package/dist/feed.js +33 -0
- package/dist/field-codec.d.ts +11 -0
- package/dist/field-codec.d.ts.map +1 -0
- package/dist/field-codec.js +48 -0
- package/dist/field-rules.d.ts +57 -0
- package/dist/field-rules.d.ts.map +1 -0
- package/dist/field-rules.js +1 -0
- package/dist/field-type.d.ts +51 -0
- package/dist/field-type.d.ts.map +1 -0
- package/dist/field-type.js +106 -0
- package/dist/fields.d.ts +295 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +1 -0
- package/dist/function.d.ts +126 -0
- package/dist/function.d.ts.map +1 -0
- package/dist/function.js +1 -0
- package/dist/functions.d.ts +18 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +99 -0
- package/dist/group-by.d.ts +10 -0
- package/dist/group-by.d.ts.map +1 -0
- package/dist/group-by.js +145 -0
- package/dist/group-having.d.ts +8 -0
- package/dist/group-having.d.ts.map +1 -0
- package/dist/group-having.js +97 -0
- package/dist/hooks.d.ts +41 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +149 -0
- package/dist/hosts.d.ts +3 -0
- package/dist/hosts.d.ts.map +1 -0
- package/dist/hosts.js +8 -0
- package/dist/identify.d.ts +15 -0
- package/dist/identify.d.ts.map +1 -0
- package/dist/identify.js +71 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/internal.d.ts +28 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +39 -0
- package/dist/lifecycle.d.ts +21 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +149 -0
- package/dist/list-update.d.ts +12 -0
- package/dist/list-update.d.ts.map +1 -0
- package/dist/list-update.js +52 -0
- package/dist/live.d.ts +37 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +139 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +60 -0
- package/dist/memory-aggregate.d.ts +7 -0
- package/dist/memory-aggregate.d.ts.map +1 -0
- package/dist/memory-aggregate.js +107 -0
- package/dist/memory-conditions.d.ts +8 -0
- package/dist/memory-conditions.d.ts.map +1 -0
- package/dist/memory-conditions.js +82 -0
- package/dist/memory-draft.d.ts +34 -0
- package/dist/memory-draft.d.ts.map +1 -0
- package/dist/memory-draft.js +255 -0
- package/dist/memory-json.d.ts +6 -0
- package/dist/memory-json.d.ts.map +1 -0
- package/dist/memory-json.js +86 -0
- package/dist/memory-read.d.ts +45 -0
- package/dist/memory-read.d.ts.map +1 -0
- package/dist/memory-read.js +244 -0
- package/dist/memory-search.d.ts +16 -0
- package/dist/memory-search.d.ts.map +1 -0
- package/dist/memory-search.js +110 -0
- package/dist/memory-values.d.ts +23 -0
- package/dist/memory-values.d.ts.map +1 -0
- package/dist/memory-values.js +82 -0
- package/dist/memory.d.ts +4 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +157 -0
- package/dist/migrate.d.ts +22 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +131 -0
- package/dist/model.d.ts +239 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +1 -0
- package/dist/nested-write.d.ts +64 -0
- package/dist/nested-write.d.ts.map +1 -0
- package/dist/nested-write.js +274 -0
- package/dist/number-update.d.ts +18 -0
- package/dist/number-update.d.ts.map +1 -0
- package/dist/number-update.js +96 -0
- package/dist/plugin-app.d.ts +48 -0
- package/dist/plugin-app.d.ts.map +1 -0
- package/dist/plugin-app.js +230 -0
- package/dist/plugin-code.d.ts +17 -0
- package/dist/plugin-code.d.ts.map +1 -0
- package/dist/plugin-code.js +125 -0
- package/dist/plugin-use.d.ts +22 -0
- package/dist/plugin-use.d.ts.map +1 -0
- package/dist/plugin-use.js +161 -0
- package/dist/plugin.d.ts +357 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +17 -0
- package/dist/query-aggregate.d.ts +82 -0
- package/dist/query-aggregate.d.ts.map +1 -0
- package/dist/query-aggregate.js +1 -0
- package/dist/query-args.d.ts +41 -0
- package/dist/query-args.d.ts.map +1 -0
- package/dist/query-args.js +263 -0
- package/dist/query-bulk.d.ts +35 -0
- package/dist/query-bulk.d.ts.map +1 -0
- package/dist/query-bulk.js +1 -0
- package/dist/query-conditions.d.ts +9 -0
- package/dist/query-conditions.d.ts.map +1 -0
- package/dist/query-conditions.js +46 -0
- package/dist/query-context.d.ts +32 -0
- package/dist/query-context.d.ts.map +1 -0
- package/dist/query-context.js +55 -0
- package/dist/query-filter.d.ts +46 -0
- package/dist/query-filter.d.ts.map +1 -0
- package/dist/query-filter.js +1 -0
- package/dist/query-group.d.ts +82 -0
- package/dist/query-group.d.ts.map +1 -0
- package/dist/query-group.js +1 -0
- package/dist/query-json.d.ts +9 -0
- package/dist/query-json.d.ts.map +1 -0
- package/dist/query-json.js +66 -0
- package/dist/query-order.d.ts +15 -0
- package/dist/query-order.d.ts.map +1 -0
- package/dist/query-order.js +115 -0
- package/dist/query-project.d.ts +15 -0
- package/dist/query-project.d.ts.map +1 -0
- package/dist/query-project.js +45 -0
- package/dist/query-search.d.ts +24 -0
- package/dist/query-search.d.ts.map +1 -0
- package/dist/query-search.js +2 -0
- package/dist/query-values.d.ts +28 -0
- package/dist/query-values.d.ts.map +1 -0
- package/dist/query-values.js +91 -0
- package/dist/query-where.d.ts +46 -0
- package/dist/query-where.d.ts.map +1 -0
- package/dist/query-where.js +200 -0
- package/dist/query-write.d.ts +14 -0
- package/dist/query-write.d.ts.map +1 -0
- package/dist/query-write.js +260 -0
- package/dist/query.d.ts +254 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +1 -0
- package/dist/record.d.ts +257 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +1 -0
- package/dist/recorder.d.ts +32 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +152 -0
- package/dist/relation-write.d.ts +41 -0
- package/dist/relation-write.d.ts.map +1 -0
- package/dist/relation-write.js +112 -0
- package/dist/relations.d.ts +42 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +119 -0
- package/dist/rules.d.ts +97 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +71 -0
- package/dist/running.d.ts +29 -0
- package/dist/running.d.ts.map +1 -0
- package/dist/running.js +39 -0
- package/dist/schema-indexes.d.ts +11 -0
- package/dist/schema-indexes.d.ts.map +1 -0
- package/dist/schema-indexes.js +59 -0
- package/dist/schema-virtuals.d.ts +19 -0
- package/dist/schema-virtuals.d.ts.map +1 -0
- package/dist/schema-virtuals.js +100 -0
- package/dist/schema.d.ts +35 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +267 -0
- package/dist/scope.d.ts +46 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +82 -0
- package/dist/scratch.d.ts +14 -0
- package/dist/scratch.d.ts.map +1 -0
- package/dist/scratch.js +10 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +4 -0
- package/dist/symbols.d.ts +10 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +1 -0
- package/dist/transaction.d.ts +28 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +156 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/update-many.d.ts +7 -0
- package/dist/update-many.d.ts.map +1 -0
- package/dist/update-many.js +182 -0
- package/dist/uuid.d.ts +7 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +48 -0
- package/dist/validation.d.ts +24 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +72 -0
- package/dist/virtual-read.d.ts +9 -0
- package/dist/virtual-read.d.ts.map +1 -0
- package/dist/virtual-read.js +145 -0
- package/dist/virtual.d.ts +142 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1 -0
- package/dist/write-context.d.ts +46 -0
- package/dist/write-context.d.ts.map +1 -0
- package/dist/write-context.js +102 -0
- package/dist/write-data.d.ts +26 -0
- package/dist/write-data.d.ts.map +1 -0
- package/dist/write-data.js +144 -0
- package/dist/write-plan.d.ts +107 -0
- package/dist/write-plan.d.ts.map +1 -0
- package/dist/write-plan.js +170 -0
- package/dist/write-target.d.ts +17 -0
- package/dist/write-target.d.ts.map +1 -0
- package/dist/write-target.js +59 -0
- package/dist/writes.d.ts +23 -0
- package/dist/writes.d.ts.map +1 -0
- package/dist/writes.js +246 -0
- package/dist/writing.d.ts +10 -0
- package/dist/writing.d.ts.map +1 -0
- package/dist/writing.js +66 -0
- package/package.json +42 -3
- package/src/access.ts +329 -0
- package/src/adapter-errors.ts +89 -0
- package/src/adapter-json.ts +21 -0
- package/src/adapter-migrations.ts +98 -0
- package/src/adapter.ts +363 -0
- package/src/aggregate.ts +136 -0
- package/src/app-internals.ts +26 -0
- package/src/app-options.ts +24 -0
- package/src/cascade.ts +297 -0
- package/src/cli-config.ts +45 -0
- package/src/cli-deploy.ts +235 -0
- package/src/cli-main.ts +165 -0
- package/src/cli-migrate.ts +143 -0
- package/src/cli.ts +5 -0
- package/src/complete.ts +73 -0
- package/src/console-logger.ts +87 -0
- package/src/create-many.ts +126 -0
- package/src/decimal.ts +74 -0
- package/src/define-app.ts +108 -0
- package/src/define-enum.ts +26 -0
- package/src/define-function.ts +188 -0
- package/src/define-model.ts +220 -0
- package/src/definition.ts +352 -0
- package/src/delete-many.ts +76 -0
- package/src/engine.ts +281 -0
- package/src/errors.ts +97 -0
- package/src/exact.ts +49 -0
- package/src/feed.ts +39 -0
- package/src/field-codec.ts +72 -0
- package/src/field-rules.ts +77 -0
- package/src/field-type.ts +219 -0
- package/src/fields.ts +383 -0
- package/src/function.ts +143 -0
- package/src/functions.ts +154 -0
- package/src/group-by.ts +186 -0
- package/src/group-having.ts +143 -0
- package/src/hooks.ts +215 -0
- package/src/hosts.ts +11 -0
- package/src/identify.ts +95 -0
- package/src/index.ts +125 -0
- package/src/internal.ts +68 -0
- package/src/lifecycle.ts +214 -0
- package/src/list-update.ts +71 -0
- package/src/live.ts +199 -0
- package/src/manifest.ts +71 -0
- package/src/memory-aggregate.ts +162 -0
- package/src/memory-conditions.ts +88 -0
- package/src/memory-draft.ts +319 -0
- package/src/memory-json.ts +91 -0
- package/src/memory-read.ts +311 -0
- package/src/memory-search.ts +149 -0
- package/src/memory-values.ts +106 -0
- package/src/memory.ts +173 -0
- package/src/migrate.ts +152 -0
- package/src/model.ts +312 -0
- package/src/nested-write.ts +390 -0
- package/src/number-update.ts +142 -0
- package/src/plugin-app.ts +308 -0
- package/src/plugin-code.ts +158 -0
- package/src/plugin-use.ts +217 -0
- package/src/plugin.ts +400 -0
- package/src/query-aggregate.ts +96 -0
- package/src/query-args.ts +375 -0
- package/src/query-bulk.ts +53 -0
- package/src/query-conditions.ts +53 -0
- package/src/query-context.ts +104 -0
- package/src/query-filter.ts +54 -0
- package/src/query-group.ts +137 -0
- package/src/query-json.ts +90 -0
- package/src/query-order.ts +158 -0
- package/src/query-project.ts +73 -0
- package/src/query-search.ts +31 -0
- package/src/query-values.ts +131 -0
- package/src/query-where.ts +318 -0
- package/src/query-write.ts +360 -0
- package/src/query.ts +400 -0
- package/src/record.ts +379 -0
- package/src/recorder.ts +212 -0
- package/src/relation-write.ts +176 -0
- package/src/relations.ts +165 -0
- package/src/rules.ts +188 -0
- package/src/running.ts +62 -0
- package/src/schema-indexes.ts +88 -0
- package/src/schema-virtuals.ts +134 -0
- package/src/schema.ts +367 -0
- package/src/scope.ts +123 -0
- package/src/scratch.ts +28 -0
- package/src/serve.ts +5 -0
- package/src/symbols.ts +11 -0
- package/src/transaction.ts +211 -0
- package/src/types.ts +79 -0
- package/src/update-many.ts +268 -0
- package/src/uuid.ts +51 -0
- package/src/validation.ts +107 -0
- package/src/virtual-read.ts +198 -0
- package/src/virtual.ts +187 -0
- package/src/write-context.ts +170 -0
- package/src/write-data.ts +188 -0
- package/src/write-plan.ts +273 -0
- package/src/write-target.ts +92 -0
- package/src/writes.ts +361 -0
- package/src/writing.ts +93 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// How `memory()` tests a json value at a path (D-149), as PostgreSQL's
|
|
2
|
+
// `#>`, `@>` and jsonb comparisons do (D-94).
|
|
3
|
+
import type { Condition } from './adapter.ts'
|
|
4
|
+
import { compareValues, lower, same, text } from './memory-values.ts'
|
|
5
|
+
import { isObject } from './query-where.ts'
|
|
6
|
+
|
|
7
|
+
type Truth = boolean | null
|
|
8
|
+
|
|
9
|
+
/** An array's index as PostgreSQL's `strtoint` reads it; `undefined` when not one. */
|
|
10
|
+
function indexOf(key: string, length: number): number | undefined {
|
|
11
|
+
if (!/^[\t\n\v\f\r ]*[+-]?\d+$/.test(key)) return undefined
|
|
12
|
+
const index = Number(key)
|
|
13
|
+
if (index < -2_147_483_647 || index > 2_147_483_647) return undefined
|
|
14
|
+
return index < 0 ? length + index : index
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The value at `path`, `undefined` where the value lacks it. */
|
|
18
|
+
function at(value: unknown, path: readonly string[]): unknown {
|
|
19
|
+
let current = value
|
|
20
|
+
for (const key of path) {
|
|
21
|
+
if (Array.isArray(current)) {
|
|
22
|
+
const index = indexOf(key, current.length)
|
|
23
|
+
current = index === undefined || index < 0 ? undefined : current[index]
|
|
24
|
+
} else if (isObject(current) && Object.hasOwn(current, key)) {
|
|
25
|
+
current = current[key]
|
|
26
|
+
} else return undefined
|
|
27
|
+
}
|
|
28
|
+
return current
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `@>`: a nested value is held only by a value of its own JSON type. */
|
|
32
|
+
function holds(container: unknown, item: unknown): boolean {
|
|
33
|
+
if (Array.isArray(container) && Array.isArray(item)) {
|
|
34
|
+
return item.every((each) => container.some((one) => holds(one, each)))
|
|
35
|
+
}
|
|
36
|
+
if (isObject(container) && isObject(item)) {
|
|
37
|
+
return Object.entries(item).every(
|
|
38
|
+
([key, each]) =>
|
|
39
|
+
Object.hasOwn(container, key) && holds(container[key], each),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
return (
|
|
43
|
+
!Array.isArray(container) && !isObject(container) && same(container, item)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const typeOf = (value: unknown): string =>
|
|
48
|
+
Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value
|
|
49
|
+
|
|
50
|
+
/** A json condition with `path`: unknown where the value lacks the path. */
|
|
51
|
+
export function jsonPath(stored: unknown, condition: Condition): Truth {
|
|
52
|
+
const value = stored === null ? undefined : at(stored, condition.path ?? [])
|
|
53
|
+
if (value === undefined) return null
|
|
54
|
+
const fold =
|
|
55
|
+
condition.mode === 'insensitive' ? lower : (item: unknown) => item
|
|
56
|
+
const strings = (test: (a: string, b: string) => boolean, operand: unknown) =>
|
|
57
|
+
text(fold(value), fold(operand), test)
|
|
58
|
+
const items: readonly unknown[] = Array.isArray(value) ? value : []
|
|
59
|
+
const truths: boolean[] = []
|
|
60
|
+
for (const [operator, operand] of Object.entries(condition)) {
|
|
61
|
+
if (operand === undefined || operator === 'mode' || operator === 'path')
|
|
62
|
+
continue
|
|
63
|
+
if (operator === 'equals') truths.push(same(value, operand))
|
|
64
|
+
else if (operator === 'not') truths.push(!same(value, operand))
|
|
65
|
+
else if (operator === 'stringContains') {
|
|
66
|
+
truths.push(strings((a, b) => a.includes(b), operand))
|
|
67
|
+
} else if (operator === 'stringStartsWith') {
|
|
68
|
+
truths.push(strings((a, b) => a.startsWith(b), operand))
|
|
69
|
+
} else if (operator === 'stringEndsWith') {
|
|
70
|
+
truths.push(strings((a, b) => a.endsWith(b), operand))
|
|
71
|
+
} else if (operator === 'arrayContains') {
|
|
72
|
+
truths.push(Array.isArray(value) && holds(value, operand))
|
|
73
|
+
} else if (operator === 'arrayStartsWith') {
|
|
74
|
+
truths.push(items.length > 0 && same(items[0], operand))
|
|
75
|
+
} else if (operator === 'arrayEndsWith') {
|
|
76
|
+
truths.push(items.length > 0 && same(items.at(-1), operand))
|
|
77
|
+
} else {
|
|
78
|
+
const order = compareValues(value, operand)
|
|
79
|
+
const kept =
|
|
80
|
+
operator === 'lt'
|
|
81
|
+
? order < 0
|
|
82
|
+
: operator === 'lte'
|
|
83
|
+
? order <= 0
|
|
84
|
+
: operator === 'gt'
|
|
85
|
+
? order > 0
|
|
86
|
+
: order >= 0
|
|
87
|
+
truths.push(typeOf(value) === typeOf(operand) && kept)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return !truths.includes(false)
|
|
91
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// How `memory()` reads: filters with SQL's three-valued logic, so that a
|
|
2
|
+
// `null` compares as PostgreSQL would, then order, cursor and page.
|
|
3
|
+
import type {
|
|
4
|
+
Condition,
|
|
5
|
+
Filter,
|
|
6
|
+
Nested,
|
|
7
|
+
Order,
|
|
8
|
+
Row,
|
|
9
|
+
SchemaField,
|
|
10
|
+
Step,
|
|
11
|
+
} from './adapter.ts'
|
|
12
|
+
import type { Truth } from './memory-conditions.ts'
|
|
13
|
+
import { and, isFilters, not, or, scalar } from './memory-conditions.ts'
|
|
14
|
+
import { compareKeys, copy, sortable } from './memory-values.ts'
|
|
15
|
+
import { relevance, searches } from './memory-search.ts'
|
|
16
|
+
import { modelOf } from './query-where.ts'
|
|
17
|
+
|
|
18
|
+
/** A stored row; a stored `one` key is the target's id or `null`. */
|
|
19
|
+
export type StoredRow = {
|
|
20
|
+
readonly id: string
|
|
21
|
+
readonly [field: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Store = {
|
|
25
|
+
readonly tables: Map<string, Map<string, StoredRow>>
|
|
26
|
+
/** `Model.field` → id → the linked ids, kept on both sides (D-64). */
|
|
27
|
+
readonly joins: Map<string, Map<string, Set<string>>>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A copy of the store, which a write changes and keeps once whole. */
|
|
31
|
+
export function clone(store: Store): Store {
|
|
32
|
+
return {
|
|
33
|
+
tables: new Map(
|
|
34
|
+
[...store.tables].map(([name, table]) => [name, new Map(table)]),
|
|
35
|
+
),
|
|
36
|
+
joins: new Map(
|
|
37
|
+
[...store.joins].map(([key, side]) => [
|
|
38
|
+
key,
|
|
39
|
+
new Map([...side].map(([id, ids]) => [id, new Set(ids)])),
|
|
40
|
+
]),
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Each model's fields by name. */
|
|
46
|
+
export type Fields = ReadonlyMap<string, ReadonlyMap<string, SchemaField>>
|
|
47
|
+
|
|
48
|
+
export class Reader {
|
|
49
|
+
readonly store: Store
|
|
50
|
+
readonly fields: Fields
|
|
51
|
+
/** A write's change index → the ids its delete removed, for `removedBy`. */
|
|
52
|
+
readonly removed: Map<number, ReadonlySet<string>> = new Map()
|
|
53
|
+
|
|
54
|
+
constructor(store: Store, fields: Fields) {
|
|
55
|
+
this.store = store
|
|
56
|
+
this.fields = fields
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
table(model: string): Map<string, StoredRow> {
|
|
60
|
+
return modelOf(this.store.tables, model)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
field(model: string, name: string): SchemaField | undefined {
|
|
64
|
+
return this.fields.get(model)?.get(name)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The rows a relation of `row` points to. */
|
|
68
|
+
related(model: string, field: SchemaField, row: StoredRow): StoredRow[] {
|
|
69
|
+
if (field.kind === 'scalar') return []
|
|
70
|
+
const target = this.table(field.target)
|
|
71
|
+
if (field.kind === 'one' && field.stored) {
|
|
72
|
+
const key = row[field.name]
|
|
73
|
+
const found = typeof key === 'string' ? target.get(key) : undefined
|
|
74
|
+
return found === undefined ? [] : [found]
|
|
75
|
+
}
|
|
76
|
+
if (field.kind === 'many' && field.join) {
|
|
77
|
+
// A view reads the join of its relation (D-151).
|
|
78
|
+
const name = `${model}.${field.from ?? field.name}`
|
|
79
|
+
const ids = this.store.joins.get(name)?.get(row.id)
|
|
80
|
+
return [...(ids ?? [])].flatMap((id) => target.get(id) ?? [])
|
|
81
|
+
}
|
|
82
|
+
return [...target.values()].filter(
|
|
83
|
+
(other) => other[field.inverse] === row.id,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
matches(model: string, row: StoredRow, filter: Filter | undefined): Truth {
|
|
88
|
+
if (filter === undefined) return true
|
|
89
|
+
const truths: Truth[] = []
|
|
90
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
91
|
+
if (value === undefined) continue
|
|
92
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
93
|
+
const parts = (filter[key] ?? []).map((part) =>
|
|
94
|
+
this.matches(model, row, part),
|
|
95
|
+
)
|
|
96
|
+
if (key === 'AND') truths.push(and(parts))
|
|
97
|
+
else if (key === 'OR') truths.push(or(parts))
|
|
98
|
+
else truths.push(and(parts.map(not)))
|
|
99
|
+
} else if (isFilters(value)) {
|
|
100
|
+
throw new Error(`${model}.${key} takes a condition, not a list.`)
|
|
101
|
+
} else {
|
|
102
|
+
truths.push(this.condition(model, row, key, value))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return and(truths)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private condition(
|
|
109
|
+
model: string,
|
|
110
|
+
row: StoredRow,
|
|
111
|
+
name: string,
|
|
112
|
+
condition: Condition,
|
|
113
|
+
): Truth {
|
|
114
|
+
if (condition.search !== undefined) {
|
|
115
|
+
return searches(this.fields.get(model), row, condition.search)
|
|
116
|
+
}
|
|
117
|
+
const field = this.field(model, name)
|
|
118
|
+
if (field === undefined || field.kind === 'scalar') {
|
|
119
|
+
return scalar(row[name] ?? null, condition, (value) =>
|
|
120
|
+
sortable(value, field),
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
const rows = this.related(model, field, row)
|
|
124
|
+
const test = (filter: Filter): Truth[] =>
|
|
125
|
+
rows.map((other) => this.matches(field.target, other, filter))
|
|
126
|
+
const truths: Truth[] = []
|
|
127
|
+
if (condition.is !== undefined) {
|
|
128
|
+
truths.push(
|
|
129
|
+
condition.is === null
|
|
130
|
+
? rows.length === 0
|
|
131
|
+
: test(condition.is).includes(true),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
if (condition.isNot !== undefined) {
|
|
135
|
+
truths.push(
|
|
136
|
+
condition.isNot === null
|
|
137
|
+
? rows.length > 0
|
|
138
|
+
: !test(condition.isNot).includes(true),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
if (condition.some !== undefined) {
|
|
142
|
+
truths.push(test(condition.some).includes(true))
|
|
143
|
+
}
|
|
144
|
+
if (condition.every !== undefined) {
|
|
145
|
+
truths.push(!test(condition.every).includes(false))
|
|
146
|
+
}
|
|
147
|
+
if (condition.none !== undefined) {
|
|
148
|
+
truths.push(!test(condition.none).includes(true))
|
|
149
|
+
}
|
|
150
|
+
if (condition.removedBy !== undefined) {
|
|
151
|
+
// The key names a row an earlier delete of the write removed.
|
|
152
|
+
const removed = this.removed.get(condition.removedBy)
|
|
153
|
+
if (removed === undefined) throw new Error('No such delete.')
|
|
154
|
+
const key = row[field.name]
|
|
155
|
+
truths.push(typeof key === 'string' && removed.has(key))
|
|
156
|
+
}
|
|
157
|
+
return and(truths)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The row `through` reaches from `row`, and its model, if one (D-134). */
|
|
161
|
+
reach(
|
|
162
|
+
model: string,
|
|
163
|
+
row: StoredRow,
|
|
164
|
+
through: readonly Step[] = [],
|
|
165
|
+
): { readonly model: string; readonly row: StoredRow } | undefined {
|
|
166
|
+
let at: StoredRow | undefined = row
|
|
167
|
+
let on = model
|
|
168
|
+
for (const step of through) {
|
|
169
|
+
const field = this.field(on, step.relation)
|
|
170
|
+
if (field === undefined || field.kind === 'scalar') return undefined
|
|
171
|
+
const [next] = this.related(on, field, at)
|
|
172
|
+
at =
|
|
173
|
+
next !== undefined &&
|
|
174
|
+
this.matches(field.target, next, step.where) === true
|
|
175
|
+
? next
|
|
176
|
+
: undefined
|
|
177
|
+
if (at === undefined) return undefined
|
|
178
|
+
on = field.target
|
|
179
|
+
}
|
|
180
|
+
return { model: on, row: at }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A row's sort key: a field, through `one` relations, a count (D-134), a relevance (D-148). */
|
|
184
|
+
private key(model: string, row: StoredRow, order: Order): unknown {
|
|
185
|
+
if (order.relevance !== undefined) {
|
|
186
|
+
return relevance(this.fields.get(model), row, order.relevance)
|
|
187
|
+
}
|
|
188
|
+
const reached = this.reach(model, row, order.through)
|
|
189
|
+
if (reached === undefined) return null
|
|
190
|
+
const { model: on, row: at } = reached
|
|
191
|
+
if (order.count === undefined) {
|
|
192
|
+
return sortable(at[order.field] ?? null, this.field(on, order.field))
|
|
193
|
+
}
|
|
194
|
+
return this.count(on, at, order.field, order.count)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The rows of a `many` relation of `row` that `where` selects. */
|
|
198
|
+
private count(
|
|
199
|
+
model: string,
|
|
200
|
+
row: StoredRow,
|
|
201
|
+
name: string,
|
|
202
|
+
where: Filter,
|
|
203
|
+
): number {
|
|
204
|
+
const field = this.field(model, name)
|
|
205
|
+
if (field === undefined || field.kind === 'scalar') return 0
|
|
206
|
+
return this.related(model, field, row).filter(
|
|
207
|
+
(other) => this.matches(field.target, other, where) === true,
|
|
208
|
+
).length
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Each row with its sort keys. */
|
|
212
|
+
private keyed(
|
|
213
|
+
model: string,
|
|
214
|
+
rows: readonly StoredRow[],
|
|
215
|
+
order: readonly Order[],
|
|
216
|
+
) {
|
|
217
|
+
return rows.map((row) => ({
|
|
218
|
+
row,
|
|
219
|
+
keys: order.map((each) => this.key(model, row, each)),
|
|
220
|
+
}))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Filters, orders and pages rows of `model`, as `read` asks. */
|
|
224
|
+
select(model: string, rows: StoredRow[], read: Nested): StoredRow[] {
|
|
225
|
+
const order = read.orderBy ?? [{ field: 'id', direction: 'asc' }]
|
|
226
|
+
const compare = (a: Keyed, b: Keyed) => compareKeys(a.keys, b.keys, order)
|
|
227
|
+
const found = this.keyed(
|
|
228
|
+
model,
|
|
229
|
+
rows.filter((row) => this.matches(model, row, read.where) === true),
|
|
230
|
+
order,
|
|
231
|
+
).toSorted(compare)
|
|
232
|
+
const skip = read.skip ?? 0
|
|
233
|
+
const take = read.take
|
|
234
|
+
if (take === 0) return []
|
|
235
|
+
let start = 0
|
|
236
|
+
let end = found.length
|
|
237
|
+
if (read.cursor !== undefined) {
|
|
238
|
+
const row = [...this.table(model).values()].find(
|
|
239
|
+
(each) => this.matches(model, each, read.cursor) === true,
|
|
240
|
+
)
|
|
241
|
+
if (row === undefined) return []
|
|
242
|
+
const [cursor] = this.keyed(model, [row], order)
|
|
243
|
+
if (cursor === undefined) return []
|
|
244
|
+
if (take === undefined || take > 0) {
|
|
245
|
+
const index = found.findIndex((each) => compare(each, cursor) >= 0)
|
|
246
|
+
start = index === -1 ? found.length : index
|
|
247
|
+
} else {
|
|
248
|
+
end = found.findLastIndex((each) => compare(each, cursor) <= 0)
|
|
249
|
+
end += 1
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const sorted = found.map((each) => each.row)
|
|
253
|
+
if (take === undefined || take > 0) {
|
|
254
|
+
start += skip
|
|
255
|
+
return sorted.slice(start, take === undefined ? end : start + take)
|
|
256
|
+
}
|
|
257
|
+
end -= skip
|
|
258
|
+
return end <= 0 ? [] : sorted.slice(Math.max(start, end + take), end)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A row as the adapter returns it, with the relations `read` includes. */
|
|
262
|
+
output(model: string, row: StoredRow, read: Nested): Row {
|
|
263
|
+
const result: { id: string; [field: string]: unknown } = { id: row.id }
|
|
264
|
+
const fields = read.fields === undefined ? undefined : new Set(read.fields)
|
|
265
|
+
const reads = (name: string) => fields?.has(name) ?? true
|
|
266
|
+
for (const [key, value] of Object.entries(row)) {
|
|
267
|
+
const field = this.field(model, key)
|
|
268
|
+
if (!reads(key)) continue
|
|
269
|
+
if (field?.kind === 'one') {
|
|
270
|
+
result[key] = typeof value === 'string' ? { id: value } : null
|
|
271
|
+
} else {
|
|
272
|
+
result[key] = copy(value)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const field of this.fields.get(model)?.values() ?? []) {
|
|
276
|
+
if (field.kind === 'one' && !field.stored && reads(field.name)) {
|
|
277
|
+
const [other] = this.related(model, field, row)
|
|
278
|
+
result[field.name] = other === undefined ? null : { id: other.id }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const [name, nested] of Object.entries(read.include ?? {})) {
|
|
282
|
+
const field = this.field(model, name)
|
|
283
|
+
if (field === undefined || field.kind === 'scalar') continue
|
|
284
|
+
const rows = this.related(model, field, row)
|
|
285
|
+
if (field.kind === 'one') {
|
|
286
|
+
const [other] = rows
|
|
287
|
+
result[name] =
|
|
288
|
+
other !== undefined &&
|
|
289
|
+
this.matches(field.target, other, nested.where) === true
|
|
290
|
+
? this.output(field.target, other, nested)
|
|
291
|
+
: null
|
|
292
|
+
} else {
|
|
293
|
+
result[name] = this.select(field.target, rows, nested).map((other) =>
|
|
294
|
+
this.output(field.target, other, nested),
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const counts = Object.entries(read.count ?? {})
|
|
299
|
+
if (counts.length > 0) {
|
|
300
|
+
result['_count'] = Object.fromEntries(
|
|
301
|
+
counts.map(([name, where]) => [
|
|
302
|
+
name,
|
|
303
|
+
this.count(model, row, name, where),
|
|
304
|
+
]),
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
return result
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
type Keyed = { readonly row: StoredRow; readonly keys: readonly unknown[] }
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// `memory()`'s full-text search (D-148), close to PostgreSQL's 'simple'
|
|
2
|
+
// configuration: a word is a run of letters and digits, lowered, with no
|
|
3
|
+
// stop words and no stems; the relevance follows `ts_rank`'s formulas.
|
|
4
|
+
import type { SchemaField } from './adapter.ts'
|
|
5
|
+
import { searchedFields } from './query-search.ts'
|
|
6
|
+
|
|
7
|
+
/** A word or a phrase, each `-` before it negating it. */
|
|
8
|
+
type Term = { readonly words: readonly string[]; readonly minus: number }
|
|
9
|
+
|
|
10
|
+
/** Each word of a document and its positions, from 1. */
|
|
11
|
+
type Document = ReadonlyMap<string, readonly number[]>
|
|
12
|
+
|
|
13
|
+
type Fields = ReadonlyMap<string, SchemaField> | undefined
|
|
14
|
+
|
|
15
|
+
const words = (text: string): string[] =>
|
|
16
|
+
text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []
|
|
17
|
+
|
|
18
|
+
/** A quoted phrase, a `-`, an `or`, or a word and what follows it to a space. */
|
|
19
|
+
const tokens = /"([^"]*)"?|-|or(?![\p{L}\p{N}])|[\p{L}\p{N}][^\s"]*/giu
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `websearch_to_tsquery`'s groups of terms: an `or` between two terms
|
|
23
|
+
* separates them, another is a word. A word of several words, `l'arbre`,
|
|
24
|
+
* is a phrase.
|
|
25
|
+
*/
|
|
26
|
+
function parse(text: string): Term[][] {
|
|
27
|
+
const groups: Term[][] = [[]]
|
|
28
|
+
let minus = 0
|
|
29
|
+
let operand = false
|
|
30
|
+
for (const { 0: token, 1: quoted, index } of text.matchAll(tokens)) {
|
|
31
|
+
if (token === '-') {
|
|
32
|
+
minus += 1
|
|
33
|
+
operand = false
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
const rest = text.slice(index + token.length)
|
|
37
|
+
if (
|
|
38
|
+
quoted === undefined &&
|
|
39
|
+
token.toLowerCase() === 'or' &&
|
|
40
|
+
operand &&
|
|
41
|
+
/\S/u.test(rest)
|
|
42
|
+
) {
|
|
43
|
+
groups.push([])
|
|
44
|
+
operand = false
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
const found = words(quoted ?? token)
|
|
48
|
+
if (found.length > 0) groups.at(-1)?.push({ words: found, minus })
|
|
49
|
+
minus = 0
|
|
50
|
+
operand = true
|
|
51
|
+
}
|
|
52
|
+
return groups.filter((group) => group.length > 0)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The searched fields of `row`, in declaration order, as one document. */
|
|
56
|
+
function documentOf(
|
|
57
|
+
fields: Fields,
|
|
58
|
+
row: { readonly [field: string]: unknown },
|
|
59
|
+
): Document {
|
|
60
|
+
const texts = searchedFields(fields?.values() ?? []).map((name) => row[name])
|
|
61
|
+
const result = new Map<string, number[]>()
|
|
62
|
+
const all = texts.filter((text) => typeof text === 'string').join(' ')
|
|
63
|
+
for (const [at, word] of words(all).entries()) {
|
|
64
|
+
result.set(word, [...(result.get(word) ?? []), at + 1])
|
|
65
|
+
}
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Whether the words of a phrase follow each other in `found`. */
|
|
70
|
+
function has(found: Document, [first = '', ...rest]: readonly string[]) {
|
|
71
|
+
return (found.get(first) ?? []).some((at) =>
|
|
72
|
+
rest.every((word, index) => found.get(word)?.includes(at + index + 1)),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Whether `row` matches the search `text`; a text with no words matches none. */
|
|
77
|
+
export function searches(
|
|
78
|
+
fields: Fields,
|
|
79
|
+
row: { readonly [field: string]: unknown },
|
|
80
|
+
text: string,
|
|
81
|
+
): boolean {
|
|
82
|
+
const found = documentOf(fields, row)
|
|
83
|
+
return parse(text).some((group) =>
|
|
84
|
+
group.every((term) => has(found, term.words) === (term.minus % 2 === 0)),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** An unweighted word's weight in `ts_rank`. */
|
|
89
|
+
const weight = 0.1
|
|
90
|
+
|
|
91
|
+
/** `ts_rank`'s `calc_rank_or`: each word found, by its occurrences. */
|
|
92
|
+
function rankOr(found: Document, items: readonly string[]): number {
|
|
93
|
+
let sum = 0
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
const positions = found.get(item) ?? []
|
|
96
|
+
if (positions.length === 0) continue
|
|
97
|
+
let occurrences = 0
|
|
98
|
+
for (const at of positions.keys()) occurrences += weight / (at + 1) ** 2
|
|
99
|
+
// The sum of 1/i² tends to π²/6.
|
|
100
|
+
sum += occurrences / 1.64493406685
|
|
101
|
+
}
|
|
102
|
+
return sum / items.length
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** How near two positions are in `calc_rank_and`. */
|
|
106
|
+
const nearness = (distance: number): number =>
|
|
107
|
+
distance > 100 ? 1e-30 : 1 / (1.005 + 0.05 * Math.exp(distance / 1.5 - 2))
|
|
108
|
+
|
|
109
|
+
/** `ts_rank`'s `calc_rank_and`: each pair of words found, by their distance. */
|
|
110
|
+
function rankAnd(found: Document, items: readonly string[]): number {
|
|
111
|
+
let rank = -1
|
|
112
|
+
const lists = items.map((item) => found.get(item) ?? [])
|
|
113
|
+
for (const [index, mine] of lists.entries()) {
|
|
114
|
+
for (const theirs of lists.slice(0, index)) {
|
|
115
|
+
for (const a of mine) {
|
|
116
|
+
for (const b of theirs) {
|
|
117
|
+
if (a === b) continue
|
|
118
|
+
const near = Math.sqrt(weight * weight * nearness(Math.abs(a - b)))
|
|
119
|
+
rank = rank < 0 ? near : 1 - (1 - rank) * (1 - near)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return rank
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* How well `row` matches `text`, as `ts_rank` computes it in a `real`: an
|
|
129
|
+
* `and` of words or a phrase ranks their nearness, any other query its
|
|
130
|
+
* words' occurrences.
|
|
131
|
+
*/
|
|
132
|
+
export function relevance(
|
|
133
|
+
fields: Fields,
|
|
134
|
+
row: { readonly [field: string]: unknown },
|
|
135
|
+
text: string,
|
|
136
|
+
): number {
|
|
137
|
+
const found = documentOf(fields, row)
|
|
138
|
+
const groups = parse(text)
|
|
139
|
+
const terms = groups.flat()
|
|
140
|
+
if (found.size === 0 || terms.length === 0) return 0
|
|
141
|
+
const items = [...new Set(terms.flatMap((term) => term.words))]
|
|
142
|
+
const [only] = terms
|
|
143
|
+
const and =
|
|
144
|
+
groups.length === 1 &&
|
|
145
|
+
(terms.length > 1 || (only?.minus === 0 && only.words.length > 1))
|
|
146
|
+
const rank =
|
|
147
|
+
and && items.length > 1 ? rankAnd(found, items) : rankOr(found, items)
|
|
148
|
+
return Math.fround(rank < 0 ? 1e-20 : rank)
|
|
149
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// How `memory()` compares and copies values, as PostgreSQL would (D-94).
|
|
2
|
+
import type { SchemaField, Sort } from './adapter.ts'
|
|
3
|
+
import { unitsOf } from './decimal.ts'
|
|
4
|
+
import { isObject } from './query-where.ts'
|
|
5
|
+
|
|
6
|
+
/** Equality as PostgreSQL's: `-0` is `0`, json keys come in any order. */
|
|
7
|
+
export function same(a: unknown, b: unknown): boolean {
|
|
8
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
|
|
9
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
10
|
+
return (
|
|
11
|
+
a.length === b.length && a.every((item, index) => same(item, b[index]))
|
|
12
|
+
)
|
|
13
|
+
if (isObject(a) && isObject(b)) {
|
|
14
|
+
const keys = Object.keys(a)
|
|
15
|
+
return (
|
|
16
|
+
keys.length === Object.keys(b).length &&
|
|
17
|
+
keys.every((key) => Object.hasOwn(b, key) && same(a[key], b[key]))
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
return a === b
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A surrogate sorts above U+FFFF, as the code point it is part of. */
|
|
24
|
+
const weight = (unit: number): number => {
|
|
25
|
+
if (unit >= 0xd8_00 && unit <= 0xdf_ff) return unit + 0x20_00
|
|
26
|
+
return unit >= 0xe0_00 ? unit - 0x8_00 : unit
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A value as it sorts: a decimal as its units, by value (D-145). */
|
|
30
|
+
export const sortable = (
|
|
31
|
+
value: unknown,
|
|
32
|
+
field: SchemaField | undefined,
|
|
33
|
+
): unknown =>
|
|
34
|
+
field?.kind === 'scalar' &&
|
|
35
|
+
field.type === 'decimal' &&
|
|
36
|
+
typeof value === 'string'
|
|
37
|
+
? unitsOf(value, field.scale ?? 0)
|
|
38
|
+
: value
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Strings compare by code point, as PostgreSQL's `C` collation (D-94);
|
|
42
|
+
* dates, numbers and booleans by their number, a decimal's units exactly.
|
|
43
|
+
* Json values are not sorted.
|
|
44
|
+
*/
|
|
45
|
+
export function compareValues(a: unknown, b: unknown): number {
|
|
46
|
+
if (typeof a === 'bigint' && typeof b === 'bigint') {
|
|
47
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
48
|
+
}
|
|
49
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
50
|
+
for (let index = 0; index < Math.min(a.length, b.length); index++) {
|
|
51
|
+
const order = weight(a.charCodeAt(index)) - weight(b.charCodeAt(index))
|
|
52
|
+
if (order !== 0) return order
|
|
53
|
+
}
|
|
54
|
+
return a.length - b.length
|
|
55
|
+
}
|
|
56
|
+
return Number(a) - Number(b)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `null` sorts as `nulls` says, by default last ascending and first
|
|
61
|
+
* descending, as in PostgreSQL (D-149).
|
|
62
|
+
*/
|
|
63
|
+
export function compareKeys(
|
|
64
|
+
a: readonly unknown[],
|
|
65
|
+
b: readonly unknown[],
|
|
66
|
+
order: readonly Sort[],
|
|
67
|
+
): number {
|
|
68
|
+
for (const [at, { direction, nulls }] of order.entries()) {
|
|
69
|
+
const left = a[at] ?? null
|
|
70
|
+
const right = b[at] ?? null
|
|
71
|
+
if (left === null || right === null) {
|
|
72
|
+
if (left === right) continue
|
|
73
|
+
const last =
|
|
74
|
+
(nulls ?? (direction === 'asc' ? 'last' : 'first')) === 'last'
|
|
75
|
+
return (left === null) === last ? 1 : -1
|
|
76
|
+
}
|
|
77
|
+
const result = compareValues(left, right)
|
|
78
|
+
if (result !== 0) return direction === 'asc' ? result : -result
|
|
79
|
+
}
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A stored value the caller may change without changing the store. */
|
|
84
|
+
export function copy(value: unknown): unknown {
|
|
85
|
+
if (value instanceof Date) return new Date(value.getTime())
|
|
86
|
+
return typeof value === 'object' && value !== null
|
|
87
|
+
? structuredClone(value)
|
|
88
|
+
: value
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A text operator; the `where` check lets only strings reach it. */
|
|
92
|
+
export function text(
|
|
93
|
+
value: unknown,
|
|
94
|
+
operand: unknown,
|
|
95
|
+
test: (value: string, operand: string) => boolean,
|
|
96
|
+
): boolean {
|
|
97
|
+
return (
|
|
98
|
+
typeof value === 'string' &&
|
|
99
|
+
typeof operand === 'string' &&
|
|
100
|
+
test(value, operand)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A string lowered for `mode: 'insensitive'` (D-140), any other value as is. */
|
|
105
|
+
export const lower = (value: unknown): unknown =>
|
|
106
|
+
typeof value === 'string' ? value.toLowerCase() : value
|