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.
Files changed (411) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +4 -2
  3. package/dist/access.d.ts +62 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +258 -0
  6. package/dist/adapter-errors.d.ts +43 -0
  7. package/dist/adapter-errors.d.ts.map +1 -0
  8. package/dist/adapter-errors.js +72 -0
  9. package/dist/adapter-json.d.ts +20 -0
  10. package/dist/adapter-json.d.ts.map +1 -0
  11. package/dist/adapter-json.js +2 -0
  12. package/dist/adapter-migrations.d.ts +90 -0
  13. package/dist/adapter-migrations.d.ts.map +1 -0
  14. package/dist/adapter-migrations.js +1 -0
  15. package/dist/adapter.d.ts +333 -0
  16. package/dist/adapter.d.ts.map +1 -0
  17. package/dist/adapter.js +5 -0
  18. package/dist/aggregate.d.ts +31 -0
  19. package/dist/aggregate.d.ts.map +1 -0
  20. package/dist/aggregate.js +99 -0
  21. package/dist/app-internals.d.ts +10 -0
  22. package/dist/app-internals.d.ts.map +1 -0
  23. package/dist/app-internals.js +12 -0
  24. package/dist/app-options.d.ts +22 -0
  25. package/dist/app-options.d.ts.map +1 -0
  26. package/dist/app-options.js +3 -0
  27. package/dist/cascade.d.ts +47 -0
  28. package/dist/cascade.d.ts.map +1 -0
  29. package/dist/cascade.js +209 -0
  30. package/dist/cli-config.d.ts +6 -0
  31. package/dist/cli-config.d.ts.map +1 -0
  32. package/dist/cli-config.js +45 -0
  33. package/dist/cli-deploy.d.ts +14 -0
  34. package/dist/cli-deploy.d.ts.map +1 -0
  35. package/dist/cli-deploy.js +193 -0
  36. package/dist/cli-main.d.ts +16 -0
  37. package/dist/cli-main.d.ts.map +1 -0
  38. package/dist/cli-main.js +151 -0
  39. package/dist/cli-migrate.d.ts +20 -0
  40. package/dist/cli-migrate.d.ts.map +1 -0
  41. package/dist/cli-migrate.js +105 -0
  42. package/dist/cli.d.ts +3 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +4 -0
  45. package/dist/complete.d.ts +13 -0
  46. package/dist/complete.d.ts.map +1 -0
  47. package/dist/complete.js +60 -0
  48. package/dist/console-logger.d.ts +3 -0
  49. package/dist/console-logger.d.ts.map +1 -0
  50. package/dist/console-logger.js +62 -0
  51. package/dist/create-many.d.ts +7 -0
  52. package/dist/create-many.d.ts.map +1 -0
  53. package/dist/create-many.js +88 -0
  54. package/dist/decimal.d.ts +23 -0
  55. package/dist/decimal.d.ts.map +1 -0
  56. package/dist/decimal.js +50 -0
  57. package/dist/define-app.d.ts +59 -0
  58. package/dist/define-app.d.ts.map +1 -0
  59. package/dist/define-app.js +5 -0
  60. package/dist/define-enum.d.ts +6 -0
  61. package/dist/define-enum.d.ts.map +1 -0
  62. package/dist/define-enum.js +18 -0
  63. package/dist/define-function.d.ts +25 -0
  64. package/dist/define-function.d.ts.map +1 -0
  65. package/dist/define-function.js +111 -0
  66. package/dist/define-model.d.ts +48 -0
  67. package/dist/define-model.d.ts.map +1 -0
  68. package/dist/define-model.js +85 -0
  69. package/dist/definition.d.ts +179 -0
  70. package/dist/definition.d.ts.map +1 -0
  71. package/dist/definition.js +165 -0
  72. package/dist/delete-many.d.ts +13 -0
  73. package/dist/delete-many.d.ts.map +1 -0
  74. package/dist/delete-many.js +58 -0
  75. package/dist/engine.d.ts +16 -0
  76. package/dist/engine.d.ts.map +1 -0
  77. package/dist/engine.js +174 -0
  78. package/dist/errors.d.ts +50 -0
  79. package/dist/errors.d.ts.map +1 -0
  80. package/dist/errors.js +43 -0
  81. package/dist/exact.d.ts +20 -0
  82. package/dist/exact.d.ts.map +1 -0
  83. package/dist/exact.js +1 -0
  84. package/dist/feed.d.ts +6 -0
  85. package/dist/feed.d.ts.map +1 -0
  86. package/dist/feed.js +33 -0
  87. package/dist/field-codec.d.ts +11 -0
  88. package/dist/field-codec.d.ts.map +1 -0
  89. package/dist/field-codec.js +48 -0
  90. package/dist/field-rules.d.ts +57 -0
  91. package/dist/field-rules.d.ts.map +1 -0
  92. package/dist/field-rules.js +1 -0
  93. package/dist/field-type.d.ts +51 -0
  94. package/dist/field-type.d.ts.map +1 -0
  95. package/dist/field-type.js +106 -0
  96. package/dist/fields.d.ts +295 -0
  97. package/dist/fields.d.ts.map +1 -0
  98. package/dist/fields.js +1 -0
  99. package/dist/function.d.ts +126 -0
  100. package/dist/function.d.ts.map +1 -0
  101. package/dist/function.js +1 -0
  102. package/dist/functions.d.ts +18 -0
  103. package/dist/functions.d.ts.map +1 -0
  104. package/dist/functions.js +99 -0
  105. package/dist/group-by.d.ts +10 -0
  106. package/dist/group-by.d.ts.map +1 -0
  107. package/dist/group-by.js +145 -0
  108. package/dist/group-having.d.ts +8 -0
  109. package/dist/group-having.d.ts.map +1 -0
  110. package/dist/group-having.js +97 -0
  111. package/dist/hooks.d.ts +41 -0
  112. package/dist/hooks.d.ts.map +1 -0
  113. package/dist/hooks.js +149 -0
  114. package/dist/hosts.d.ts +3 -0
  115. package/dist/hosts.d.ts.map +1 -0
  116. package/dist/hosts.js +8 -0
  117. package/dist/identify.d.ts +15 -0
  118. package/dist/identify.d.ts.map +1 -0
  119. package/dist/identify.js +71 -0
  120. package/dist/index.d.ts +25 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +12 -0
  123. package/dist/internal.d.ts +28 -0
  124. package/dist/internal.d.ts.map +1 -0
  125. package/dist/internal.js +39 -0
  126. package/dist/lifecycle.d.ts +21 -0
  127. package/dist/lifecycle.d.ts.map +1 -0
  128. package/dist/lifecycle.js +149 -0
  129. package/dist/list-update.d.ts +12 -0
  130. package/dist/list-update.d.ts.map +1 -0
  131. package/dist/list-update.js +52 -0
  132. package/dist/live.d.ts +37 -0
  133. package/dist/live.d.ts.map +1 -0
  134. package/dist/live.js +139 -0
  135. package/dist/manifest.d.ts +11 -0
  136. package/dist/manifest.d.ts.map +1 -0
  137. package/dist/manifest.js +60 -0
  138. package/dist/memory-aggregate.d.ts +7 -0
  139. package/dist/memory-aggregate.d.ts.map +1 -0
  140. package/dist/memory-aggregate.js +107 -0
  141. package/dist/memory-conditions.d.ts +8 -0
  142. package/dist/memory-conditions.d.ts.map +1 -0
  143. package/dist/memory-conditions.js +82 -0
  144. package/dist/memory-draft.d.ts +34 -0
  145. package/dist/memory-draft.d.ts.map +1 -0
  146. package/dist/memory-draft.js +255 -0
  147. package/dist/memory-json.d.ts +6 -0
  148. package/dist/memory-json.d.ts.map +1 -0
  149. package/dist/memory-json.js +86 -0
  150. package/dist/memory-read.d.ts +45 -0
  151. package/dist/memory-read.d.ts.map +1 -0
  152. package/dist/memory-read.js +244 -0
  153. package/dist/memory-search.d.ts +16 -0
  154. package/dist/memory-search.d.ts.map +1 -0
  155. package/dist/memory-search.js +110 -0
  156. package/dist/memory-values.d.ts +23 -0
  157. package/dist/memory-values.d.ts.map +1 -0
  158. package/dist/memory-values.js +82 -0
  159. package/dist/memory.d.ts +4 -0
  160. package/dist/memory.d.ts.map +1 -0
  161. package/dist/memory.js +157 -0
  162. package/dist/migrate.d.ts +22 -0
  163. package/dist/migrate.d.ts.map +1 -0
  164. package/dist/migrate.js +131 -0
  165. package/dist/model.d.ts +239 -0
  166. package/dist/model.d.ts.map +1 -0
  167. package/dist/model.js +1 -0
  168. package/dist/nested-write.d.ts +64 -0
  169. package/dist/nested-write.d.ts.map +1 -0
  170. package/dist/nested-write.js +274 -0
  171. package/dist/number-update.d.ts +18 -0
  172. package/dist/number-update.d.ts.map +1 -0
  173. package/dist/number-update.js +96 -0
  174. package/dist/plugin-app.d.ts +48 -0
  175. package/dist/plugin-app.d.ts.map +1 -0
  176. package/dist/plugin-app.js +230 -0
  177. package/dist/plugin-code.d.ts +17 -0
  178. package/dist/plugin-code.d.ts.map +1 -0
  179. package/dist/plugin-code.js +125 -0
  180. package/dist/plugin-use.d.ts +22 -0
  181. package/dist/plugin-use.d.ts.map +1 -0
  182. package/dist/plugin-use.js +161 -0
  183. package/dist/plugin.d.ts +357 -0
  184. package/dist/plugin.d.ts.map +1 -0
  185. package/dist/plugin.js +17 -0
  186. package/dist/query-aggregate.d.ts +82 -0
  187. package/dist/query-aggregate.d.ts.map +1 -0
  188. package/dist/query-aggregate.js +1 -0
  189. package/dist/query-args.d.ts +41 -0
  190. package/dist/query-args.d.ts.map +1 -0
  191. package/dist/query-args.js +263 -0
  192. package/dist/query-bulk.d.ts +35 -0
  193. package/dist/query-bulk.d.ts.map +1 -0
  194. package/dist/query-bulk.js +1 -0
  195. package/dist/query-conditions.d.ts +9 -0
  196. package/dist/query-conditions.d.ts.map +1 -0
  197. package/dist/query-conditions.js +46 -0
  198. package/dist/query-context.d.ts +32 -0
  199. package/dist/query-context.d.ts.map +1 -0
  200. package/dist/query-context.js +55 -0
  201. package/dist/query-filter.d.ts +46 -0
  202. package/dist/query-filter.d.ts.map +1 -0
  203. package/dist/query-filter.js +1 -0
  204. package/dist/query-group.d.ts +82 -0
  205. package/dist/query-group.d.ts.map +1 -0
  206. package/dist/query-group.js +1 -0
  207. package/dist/query-json.d.ts +9 -0
  208. package/dist/query-json.d.ts.map +1 -0
  209. package/dist/query-json.js +66 -0
  210. package/dist/query-order.d.ts +15 -0
  211. package/dist/query-order.d.ts.map +1 -0
  212. package/dist/query-order.js +115 -0
  213. package/dist/query-project.d.ts +15 -0
  214. package/dist/query-project.d.ts.map +1 -0
  215. package/dist/query-project.js +45 -0
  216. package/dist/query-search.d.ts +24 -0
  217. package/dist/query-search.d.ts.map +1 -0
  218. package/dist/query-search.js +2 -0
  219. package/dist/query-values.d.ts +28 -0
  220. package/dist/query-values.d.ts.map +1 -0
  221. package/dist/query-values.js +91 -0
  222. package/dist/query-where.d.ts +46 -0
  223. package/dist/query-where.d.ts.map +1 -0
  224. package/dist/query-where.js +200 -0
  225. package/dist/query-write.d.ts +14 -0
  226. package/dist/query-write.d.ts.map +1 -0
  227. package/dist/query-write.js +260 -0
  228. package/dist/query.d.ts +254 -0
  229. package/dist/query.d.ts.map +1 -0
  230. package/dist/query.js +1 -0
  231. package/dist/record.d.ts +257 -0
  232. package/dist/record.d.ts.map +1 -0
  233. package/dist/record.js +1 -0
  234. package/dist/recorder.d.ts +32 -0
  235. package/dist/recorder.d.ts.map +1 -0
  236. package/dist/recorder.js +152 -0
  237. package/dist/relation-write.d.ts +41 -0
  238. package/dist/relation-write.d.ts.map +1 -0
  239. package/dist/relation-write.js +112 -0
  240. package/dist/relations.d.ts +42 -0
  241. package/dist/relations.d.ts.map +1 -0
  242. package/dist/relations.js +119 -0
  243. package/dist/rules.d.ts +97 -0
  244. package/dist/rules.d.ts.map +1 -0
  245. package/dist/rules.js +71 -0
  246. package/dist/running.d.ts +29 -0
  247. package/dist/running.d.ts.map +1 -0
  248. package/dist/running.js +39 -0
  249. package/dist/schema-indexes.d.ts +11 -0
  250. package/dist/schema-indexes.d.ts.map +1 -0
  251. package/dist/schema-indexes.js +59 -0
  252. package/dist/schema-virtuals.d.ts +19 -0
  253. package/dist/schema-virtuals.d.ts.map +1 -0
  254. package/dist/schema-virtuals.js +100 -0
  255. package/dist/schema.d.ts +35 -0
  256. package/dist/schema.d.ts.map +1 -0
  257. package/dist/schema.js +267 -0
  258. package/dist/scope.d.ts +46 -0
  259. package/dist/scope.d.ts.map +1 -0
  260. package/dist/scope.js +82 -0
  261. package/dist/scratch.d.ts +14 -0
  262. package/dist/scratch.d.ts.map +1 -0
  263. package/dist/scratch.js +10 -0
  264. package/dist/serve.d.ts +2 -0
  265. package/dist/serve.d.ts.map +1 -0
  266. package/dist/serve.js +4 -0
  267. package/dist/symbols.d.ts +10 -0
  268. package/dist/symbols.d.ts.map +1 -0
  269. package/dist/symbols.js +1 -0
  270. package/dist/transaction.d.ts +28 -0
  271. package/dist/transaction.d.ts.map +1 -0
  272. package/dist/transaction.js +156 -0
  273. package/dist/types.d.ts +58 -0
  274. package/dist/types.d.ts.map +1 -0
  275. package/dist/types.js +1 -0
  276. package/dist/update-many.d.ts +7 -0
  277. package/dist/update-many.d.ts.map +1 -0
  278. package/dist/update-many.js +182 -0
  279. package/dist/uuid.d.ts +7 -0
  280. package/dist/uuid.d.ts.map +1 -0
  281. package/dist/uuid.js +48 -0
  282. package/dist/validation.d.ts +24 -0
  283. package/dist/validation.d.ts.map +1 -0
  284. package/dist/validation.js +72 -0
  285. package/dist/virtual-read.d.ts +9 -0
  286. package/dist/virtual-read.d.ts.map +1 -0
  287. package/dist/virtual-read.js +145 -0
  288. package/dist/virtual.d.ts +142 -0
  289. package/dist/virtual.d.ts.map +1 -0
  290. package/dist/virtual.js +1 -0
  291. package/dist/write-context.d.ts +46 -0
  292. package/dist/write-context.d.ts.map +1 -0
  293. package/dist/write-context.js +102 -0
  294. package/dist/write-data.d.ts +26 -0
  295. package/dist/write-data.d.ts.map +1 -0
  296. package/dist/write-data.js +144 -0
  297. package/dist/write-plan.d.ts +107 -0
  298. package/dist/write-plan.d.ts.map +1 -0
  299. package/dist/write-plan.js +170 -0
  300. package/dist/write-target.d.ts +17 -0
  301. package/dist/write-target.d.ts.map +1 -0
  302. package/dist/write-target.js +59 -0
  303. package/dist/writes.d.ts +23 -0
  304. package/dist/writes.d.ts.map +1 -0
  305. package/dist/writes.js +246 -0
  306. package/dist/writing.d.ts +10 -0
  307. package/dist/writing.d.ts.map +1 -0
  308. package/dist/writing.js +66 -0
  309. package/package.json +42 -3
  310. package/src/access.ts +329 -0
  311. package/src/adapter-errors.ts +89 -0
  312. package/src/adapter-json.ts +21 -0
  313. package/src/adapter-migrations.ts +98 -0
  314. package/src/adapter.ts +363 -0
  315. package/src/aggregate.ts +136 -0
  316. package/src/app-internals.ts +26 -0
  317. package/src/app-options.ts +24 -0
  318. package/src/cascade.ts +297 -0
  319. package/src/cli-config.ts +45 -0
  320. package/src/cli-deploy.ts +235 -0
  321. package/src/cli-main.ts +165 -0
  322. package/src/cli-migrate.ts +143 -0
  323. package/src/cli.ts +5 -0
  324. package/src/complete.ts +73 -0
  325. package/src/console-logger.ts +87 -0
  326. package/src/create-many.ts +126 -0
  327. package/src/decimal.ts +74 -0
  328. package/src/define-app.ts +108 -0
  329. package/src/define-enum.ts +26 -0
  330. package/src/define-function.ts +188 -0
  331. package/src/define-model.ts +220 -0
  332. package/src/definition.ts +352 -0
  333. package/src/delete-many.ts +76 -0
  334. package/src/engine.ts +281 -0
  335. package/src/errors.ts +97 -0
  336. package/src/exact.ts +49 -0
  337. package/src/feed.ts +39 -0
  338. package/src/field-codec.ts +72 -0
  339. package/src/field-rules.ts +77 -0
  340. package/src/field-type.ts +219 -0
  341. package/src/fields.ts +383 -0
  342. package/src/function.ts +143 -0
  343. package/src/functions.ts +154 -0
  344. package/src/group-by.ts +186 -0
  345. package/src/group-having.ts +143 -0
  346. package/src/hooks.ts +215 -0
  347. package/src/hosts.ts +11 -0
  348. package/src/identify.ts +95 -0
  349. package/src/index.ts +125 -0
  350. package/src/internal.ts +68 -0
  351. package/src/lifecycle.ts +214 -0
  352. package/src/list-update.ts +71 -0
  353. package/src/live.ts +199 -0
  354. package/src/manifest.ts +71 -0
  355. package/src/memory-aggregate.ts +162 -0
  356. package/src/memory-conditions.ts +88 -0
  357. package/src/memory-draft.ts +319 -0
  358. package/src/memory-json.ts +91 -0
  359. package/src/memory-read.ts +311 -0
  360. package/src/memory-search.ts +149 -0
  361. package/src/memory-values.ts +106 -0
  362. package/src/memory.ts +173 -0
  363. package/src/migrate.ts +152 -0
  364. package/src/model.ts +312 -0
  365. package/src/nested-write.ts +390 -0
  366. package/src/number-update.ts +142 -0
  367. package/src/plugin-app.ts +308 -0
  368. package/src/plugin-code.ts +158 -0
  369. package/src/plugin-use.ts +217 -0
  370. package/src/plugin.ts +400 -0
  371. package/src/query-aggregate.ts +96 -0
  372. package/src/query-args.ts +375 -0
  373. package/src/query-bulk.ts +53 -0
  374. package/src/query-conditions.ts +53 -0
  375. package/src/query-context.ts +104 -0
  376. package/src/query-filter.ts +54 -0
  377. package/src/query-group.ts +137 -0
  378. package/src/query-json.ts +90 -0
  379. package/src/query-order.ts +158 -0
  380. package/src/query-project.ts +73 -0
  381. package/src/query-search.ts +31 -0
  382. package/src/query-values.ts +131 -0
  383. package/src/query-where.ts +318 -0
  384. package/src/query-write.ts +360 -0
  385. package/src/query.ts +400 -0
  386. package/src/record.ts +379 -0
  387. package/src/recorder.ts +212 -0
  388. package/src/relation-write.ts +176 -0
  389. package/src/relations.ts +165 -0
  390. package/src/rules.ts +188 -0
  391. package/src/running.ts +62 -0
  392. package/src/schema-indexes.ts +88 -0
  393. package/src/schema-virtuals.ts +134 -0
  394. package/src/schema.ts +367 -0
  395. package/src/scope.ts +123 -0
  396. package/src/scratch.ts +28 -0
  397. package/src/serve.ts +5 -0
  398. package/src/symbols.ts +11 -0
  399. package/src/transaction.ts +211 -0
  400. package/src/types.ts +79 -0
  401. package/src/update-many.ts +268 -0
  402. package/src/uuid.ts +51 -0
  403. package/src/validation.ts +107 -0
  404. package/src/virtual-read.ts +198 -0
  405. package/src/virtual.ts +187 -0
  406. package/src/write-context.ts +170 -0
  407. package/src/write-data.ts +188 -0
  408. package/src/write-plan.ts +273 -0
  409. package/src/write-target.ts +92 -0
  410. package/src/writes.ts +361 -0
  411. 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