@spooky-sync/core 0.0.1-canary.33 → 0.0.1-canary.331
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/AGENTS.md +84 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2080 -142
- package/dist/index.js +16009 -2480
- package/dist/live.d.ts +149 -0
- package/dist/live.js +739 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +373 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +392 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +439 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +477 -0
- package/dist/types.d.ts +1857 -38
- package/dist/worker-edges.js +480 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +48 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +28 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +173 -0
- package/skills/sp00ky-core/SKILL.md +32 -7
- package/skills/sp00ky-core/references/config.md +7 -1
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +203 -0
- package/src/boot/boot.saga.ts +146 -0
- package/src/boot/bucket-switch.saga.test.ts +187 -0
- package/src/boot/bucket-switch.saga.ts +133 -0
- package/src/boot/preload.saga.test.ts +80 -0
- package/src/boot/preload.saga.ts +40 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +195 -0
- package/src/client/bucket-handle.ts +352 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +74 -0
- package/src/client/services.ts +434 -0
- package/src/client/sp00ky-client.test.ts +341 -0
- package/src/client/sp00ky-client.ts +625 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +69 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +80 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +167 -0
- package/src/kernel/events.ts +57 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +148 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/live/index.test.ts +512 -0
- package/src/live/index.ts +1059 -0
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +209 -0
- package/src/modules/auth/auth.local-first.test.ts +228 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +476 -22
- package/src/modules/crdt/acceptance.ts +75 -0
- package/src/modules/crdt/bytes.ts +50 -0
- package/src/modules/crdt/crdt-field.ts +125 -0
- package/src/modules/crdt/crdt.test.ts +469 -0
- package/src/modules/crdt/hash.ts +103 -0
- package/src/modules/crdt/index.ts +757 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/crdt/mirror.ts +37 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +93 -0
- package/src/modules/ref-tables.ts +91 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +301 -0
- package/src/mutation/push.saga.ts +225 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +527 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +65 -0
- package/src/query/fetch.saga.test.ts +199 -0
- package/src/query/fetch.saga.ts +120 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/query/id-set-plan.test.ts +132 -0
- package/src/query/lifecycle.saga.test.ts +274 -0
- package/src/query/lifecycle.saga.ts +186 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +407 -0
- package/src/query/membership.saga.ts +282 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +235 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +118 -0
- package/src/query/sql.ts +157 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +208 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +209 -0
- package/src/services/database/connection-supervisor.test.ts +318 -0
- package/src/services/database/connection-supervisor.ts +422 -0
- package/src/services/database/database.query-timeout.test.ts +140 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +156 -61
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/landed-body-clear.integration.test.ts +236 -0
- package/src/services/database/local-migrator.integration.test.ts +110 -0
- package/src/services/database/local-migrator.ts +61 -16
- package/src/services/database/local.test.ts +150 -0
- package/src/services/database/local.ts +508 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +285 -6
- package/src/services/database/sqlite-cache-engine.test.ts +628 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1401 -0
- package/src/services/database/sqlite-column-reset.integration.test.ts +100 -0
- package/src/services/database/sqlite-column-reset.ts +91 -0
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
- package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
- package/src/services/database/sqlite-lock-verify.test.ts +33 -0
- package/src/services/database/sqlite-lock-verify.ts +45 -0
- package/src/services/database/sqlite-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +164 -0
- package/src/services/database/sqlite-plan-sql.ts +196 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +417 -0
- package/src/services/database/surreal-cache-engine.ts +163 -0
- package/src/services/logger/index.test.ts +132 -0
- package/src/services/logger/index.ts +189 -7
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +10 -3
- package/src/services/persistence/surrealdb.ts +4 -4
- package/src/services/stream-processor/index.ts +806 -67
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +234 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +58 -2
- package/src/services/tabs/broker-client.ts +284 -0
- package/src/services/tabs/broker.test.ts +381 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +646 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +262 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +652 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +208 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +306 -0
- package/src/state/reducers.ts +531 -0
- package/src/state/selectors.test.ts +188 -0
- package/src/state/selectors.ts +258 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +97 -0
- package/src/sync/connection.saga.ts +89 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +663 -0
- package/src/sync/policy.ts +515 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +137 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +63 -0
- package/src/testing/run-pure.test.ts +92 -0
- package/src/testing/run-pure.ts +128 -0
- package/src/testing/services.ts +17 -0
- package/src/types.ts +633 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/error-classification.test.ts +54 -0
- package/src/utils/error-classification.ts +25 -0
- package/src/utils/index.ts +89 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +29 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -167
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { QueryPlan } from '@spooky-sync/query-builder';
|
|
3
|
+
import { renderBaseSelectSurql, renderRelationFetchSurql } from './plan-render';
|
|
4
|
+
import { buildWindowMaterializationPlan } from '../../query/window-query';
|
|
5
|
+
|
|
6
|
+
describe('renderBaseSelectSurql', () => {
|
|
7
|
+
it('renders projection, where, order, limit, offset', () => {
|
|
8
|
+
const plan: QueryPlan = {
|
|
9
|
+
table: 'post',
|
|
10
|
+
select: ['title', 'body'],
|
|
11
|
+
where: [{ field: 'published', op: '=', value: true }],
|
|
12
|
+
orderBy: [['created', 'desc']],
|
|
13
|
+
limit: 10,
|
|
14
|
+
offset: 20,
|
|
15
|
+
};
|
|
16
|
+
const { sql, vars } = renderBaseSelectSurql(plan);
|
|
17
|
+
expect(sql).toBe('SELECT title, body FROM post WHERE published = $__p0 ORDER BY created desc LIMIT 10 START 20;');
|
|
18
|
+
expect(vars).toEqual({ __p0: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders OR groups and comparison ops', () => {
|
|
22
|
+
const plan: QueryPlan = {
|
|
23
|
+
table: 'game',
|
|
24
|
+
where: [
|
|
25
|
+
{ or: [{ field: 'white', op: '=', value: 'u:1' }, { field: 'black', op: '=', value: 'u:1' }] },
|
|
26
|
+
{ field: 'moves', op: '>=', value: 5 },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
const { sql, vars } = renderBaseSelectSurql(plan);
|
|
30
|
+
expect(sql).toBe('SELECT * FROM game WHERE (white = $__p0 OR black = $__p1) AND moves >= $__p2;');
|
|
31
|
+
expect(vars).toEqual({ __p0: 'u:1', __p1: 'u:1', __p2: 5 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('honors paramRef and swap without binding a new var', () => {
|
|
35
|
+
const plan: QueryPlan = {
|
|
36
|
+
table: 't',
|
|
37
|
+
where: [{ field: 'tags', op: 'CONTAINS', value: undefined, paramRef: 'tag', swap: true }],
|
|
38
|
+
};
|
|
39
|
+
const { sql, vars } = renderBaseSelectSurql(plan, { tag: 'x' });
|
|
40
|
+
expect(sql).toBe('SELECT * FROM t WHERE $tag CONTAINS tags;');
|
|
41
|
+
expect(vars).toEqual({ tag: 'x' });
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Regression guard for the "authors + comments don't load" class: any filter on
|
|
46
|
+
// a RecordId column (`id`) must coerce its string value with
|
|
47
|
+
// `type::record(<string> …)`. On SurrealDB `id = "thread:x"` (string) never
|
|
48
|
+
// matches a RecordId, so a base select filtered by id resolves EMPTY and its
|
|
49
|
+
// whole `.related()` subtree (author, comments) loads nothing. The string-based
|
|
50
|
+
// MemStore in relation-resolver.test.ts can't catch this (it compares keys as
|
|
51
|
+
// strings), so these assert the rendered SurrealQL directly — both the parent
|
|
52
|
+
// (base select) and child (relation fetch) sides.
|
|
53
|
+
describe('record-id coercion (authors/comments loading regression)', () => {
|
|
54
|
+
it('coerces a base-select `id = <value>` filter to a record id', () => {
|
|
55
|
+
const plan: QueryPlan = { table: 'thread', where: [{ field: 'id', op: '=', value: 'thread:abc' }] };
|
|
56
|
+
const { sql, vars } = renderBaseSelectSurql(plan);
|
|
57
|
+
expect(sql).toBe('SELECT * FROM thread WHERE id = type::record(<string> $__p0);');
|
|
58
|
+
expect(vars).toEqual({ __p0: 'thread:abc' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('coerces a base-select `id = $paramRef` filter (the ThreadDetail path)', () => {
|
|
62
|
+
const plan: QueryPlan = { table: 'thread', where: [{ field: 'id', op: '=', value: undefined, paramRef: 'id' }] };
|
|
63
|
+
const { sql } = renderBaseSelectSurql(plan, { id: 'thread:abc' });
|
|
64
|
+
expect(sql).toBe('SELECT * FROM thread WHERE id = type::record(<string> $id);');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does NOT coerce non-id fields (plain string/bool columns stay literal)', () => {
|
|
68
|
+
const plan: QueryPlan = {
|
|
69
|
+
table: 'thread',
|
|
70
|
+
where: [{ field: 'title', op: '=', value: 'hi' }, { field: 'published', op: '=', value: true }],
|
|
71
|
+
};
|
|
72
|
+
const { sql } = renderBaseSelectSurql(plan);
|
|
73
|
+
expect(sql).toBe('SELECT * FROM thread WHERE title = $__p0 AND published = $__p1;');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Regression guard for the thread-detail "crossed results → 404" bug: a
|
|
77
|
+
// slave-mode node carries BOTH a baked `value` and a `paramRef` (= the field
|
|
78
|
+
// name). Materialization MUST bind `params[paramRef]` (the query's identity),
|
|
79
|
+
// NOT the baked value — otherwise a stale/other plan whose baked id ≠ the
|
|
80
|
+
// query's params would surface a different record's row. So rendering a plan
|
|
81
|
+
// baked to id-A with params for id-B must filter by B.
|
|
82
|
+
it('slave-mode node binds params over the baked value (records follow identity)', () => {
|
|
83
|
+
const planBakedToA: QueryPlan = {
|
|
84
|
+
table: 'thread',
|
|
85
|
+
where: [{ field: 'id', op: '=', value: 'thread:A', paramRef: 'id' }],
|
|
86
|
+
};
|
|
87
|
+
const { sql, vars } = renderBaseSelectSurql(planBakedToA, { id: 'thread:B' });
|
|
88
|
+
expect(sql).toBe('SELECT * FROM thread WHERE id = type::record(<string> $id);');
|
|
89
|
+
expect(vars).toEqual({ id: 'thread:B' }); // B wins — the baked A is ignored
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('slave-mode node falls back to the baked value when the param is absent', () => {
|
|
93
|
+
const plan: QueryPlan = {
|
|
94
|
+
table: 'thread',
|
|
95
|
+
where: [{ field: 'id', op: '=', value: 'thread:A', paramRef: 'id' }],
|
|
96
|
+
};
|
|
97
|
+
const { sql, vars } = renderBaseSelectSurql(plan, {}); // no params.id
|
|
98
|
+
expect(sql).toBe('SELECT * FROM thread WHERE id = type::record(<string> $__p0);');
|
|
99
|
+
expect(vars).toEqual({ __p0: 'thread:A' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('coerces `id` inside an OR group too', () => {
|
|
103
|
+
const plan: QueryPlan = {
|
|
104
|
+
table: 'thread',
|
|
105
|
+
where: [{ or: [{ field: 'id', op: '=', value: 'thread:a' }, { field: 'id', op: '=', value: 'thread:b' }] }],
|
|
106
|
+
};
|
|
107
|
+
const { sql } = renderBaseSelectSurql(plan);
|
|
108
|
+
expect(sql).toBe(
|
|
109
|
+
'SELECT * FROM thread WHERE (id = type::record(<string> $__p0) OR id = type::record(<string> $__p1));'
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('relation fetch coerces its matchField keys (the .118 fix — kept locked)', () => {
|
|
114
|
+
const { sql } = renderRelationFetchSurql({ table: 'user', matchField: 'id', keys: ['user:1'] });
|
|
115
|
+
expect(sql).toContain('id IN $__keys.map(|$__k| type::record(<string> $__k))');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('renderRelationFetchSurql', () => {
|
|
120
|
+
it('builds a WHERE ... IN $__keys batch fetch, omitting LIMIT', () => {
|
|
121
|
+
const { sql, vars } = renderRelationFetchSurql({
|
|
122
|
+
table: 'comment',
|
|
123
|
+
matchField: 'post',
|
|
124
|
+
keys: ['post:1', 'post:2'],
|
|
125
|
+
where: [{ field: 'hidden', op: '=', value: false }],
|
|
126
|
+
orderBy: [['rank', 'asc']],
|
|
127
|
+
});
|
|
128
|
+
expect(sql).toBe('SELECT * FROM comment WHERE post IN $__keys.map(|$__k| type::record(<string> $__k)) AND hidden = $__p0 ORDER BY rank asc;');
|
|
129
|
+
expect(vars).toEqual({ __keys: ['post:1', 'post:2'], __p0: false });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('buildWindowMaterializationPlan', () => {
|
|
134
|
+
it('restricts to the id-set for offset queries, dropping where/limit/offset', () => {
|
|
135
|
+
const plan: QueryPlan = {
|
|
136
|
+
table: 'post',
|
|
137
|
+
where: [{ field: 'x', op: '=', value: 1 }],
|
|
138
|
+
orderBy: [['created', 'desc']],
|
|
139
|
+
limit: 10,
|
|
140
|
+
offset: 20,
|
|
141
|
+
relations: [{ alias: 'a', table: 'u', cardinality: 'one', foreignKeyField: 'a' }],
|
|
142
|
+
};
|
|
143
|
+
const win = buildWindowMaterializationPlan(plan, ['post:5', 'post:6']);
|
|
144
|
+
expect(win).toEqual({
|
|
145
|
+
table: 'post',
|
|
146
|
+
orderBy: [['created', 'desc']],
|
|
147
|
+
relations: [{ alias: 'a', table: 'u', cardinality: 'one', foreignKeyField: 'a' }],
|
|
148
|
+
ids: ['post:5', 'post:6'],
|
|
149
|
+
where: undefined,
|
|
150
|
+
limit: undefined,
|
|
151
|
+
offset: undefined,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns null for non-offset queries (keep normal path)', () => {
|
|
156
|
+
expect(buildWindowMaterializationPlan({ table: 'post', limit: 10 }, ['post:1'])).toBeNull();
|
|
157
|
+
expect(buildWindowMaterializationPlan({ table: 'post', offset: 0 }, ['post:1'])).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { QueryPlan, WhereNode, WhereComparison } from '@spooky-sync/query-builder';
|
|
2
|
+
import type { OrderBy, RelationFetch } from './cache-engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render helpers that turn an engine-neutral {@link QueryPlan} into a concrete
|
|
6
|
+
* dialect. Two consumers: `SurrealCacheEngine` (SurrealQL) and, indirectly, the
|
|
7
|
+
* SQLite worker (which uses the SQL variants). Relations are NOT rendered here —
|
|
8
|
+
* they are resolved by `resolveRelations` via the {@link RelationFetch}
|
|
9
|
+
* primitive, so both engines share the exact same decomposition.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface RenderedQuery {
|
|
13
|
+
sql: string;
|
|
14
|
+
vars: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RenderCtx {
|
|
18
|
+
vars: Record<string, unknown>;
|
|
19
|
+
n: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bind(ctx: RenderCtx, value: unknown): string {
|
|
23
|
+
const name = `__p${ctx.n++}`;
|
|
24
|
+
ctx.vars[name] = value;
|
|
25
|
+
return `$${name}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderComparisonSurql(c: WhereComparison, ctx: RenderCtx): string {
|
|
29
|
+
// Prefer the query's own param (`paramRef`) so a filter materializes from
|
|
30
|
+
// `params` (the query's identity), not a baked literal — this is what slaves
|
|
31
|
+
// a query's rows to its id. A pure `$`-ref node has no `value`, so it always
|
|
32
|
+
// uses the param. A slave-mode node also carries `value` as a fallback for
|
|
33
|
+
// when the param is absent (e.g. a non-column field stripped by parseParams).
|
|
34
|
+
const useParam =
|
|
35
|
+
c.paramRef !== undefined &&
|
|
36
|
+
(c.value === undefined || Object.prototype.hasOwnProperty.call(ctx.vars, c.paramRef));
|
|
37
|
+
const rawRight = useParam ? `$${c.paramRef}` : bind(ctx, c.value);
|
|
38
|
+
// `id` is a RecordId, but correlation/filter values arrive as record-id
|
|
39
|
+
// STRINGS (e.g. "thread:abc"). On SurrealDB `id = "thread:abc"` never matches
|
|
40
|
+
// (string ≠ record), so a base select filtered by id (e.g. the ThreadDetail
|
|
41
|
+
// query `… FROM thread WHERE id = $id`) resolves empty — and its whole
|
|
42
|
+
// `.related()` subtree (author, comments) then loads nothing. Coerce with
|
|
43
|
+
// `type::record(<string> …)` (idempotent if already a RecordId), mirroring
|
|
44
|
+
// renderRelationFetchSurql's matchField coercion.
|
|
45
|
+
const right =
|
|
46
|
+
c.field === 'id' ? `type::record(<string> ${rawRight})` : rawRight;
|
|
47
|
+
return c.swap ? `${right} ${c.op} ${c.field}` : `${c.field} ${c.op} ${right}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Render a WHERE conjunction (AND of comparisons / OR-groups) to SurrealQL. */
|
|
51
|
+
export function renderWhereSurql(nodes: WhereNode[], ctx: RenderCtx): string {
|
|
52
|
+
return nodes
|
|
53
|
+
.map((node) => {
|
|
54
|
+
if ('or' in node) {
|
|
55
|
+
return `(${node.or.map((c) => renderComparisonSurql(c, ctx)).join(' OR ')})`;
|
|
56
|
+
}
|
|
57
|
+
return renderComparisonSurql(node, ctx);
|
|
58
|
+
})
|
|
59
|
+
.join(' AND ');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderOrderBy(orderBy: OrderBy): string {
|
|
63
|
+
return ` ORDER BY ${orderBy.map(([f, d]) => `${f} ${d}`).join(', ')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render the BASE of a SELECT (no relations) to SurrealQL. `params` supplies
|
|
68
|
+
* pre-existing bound params (e.g. `$__win` for windowing, or `paramRef` values)
|
|
69
|
+
* and is merged into the returned vars.
|
|
70
|
+
*/
|
|
71
|
+
export function renderBaseSelectSurql(
|
|
72
|
+
plan: QueryPlan,
|
|
73
|
+
params: Record<string, unknown> = {}
|
|
74
|
+
): RenderedQuery {
|
|
75
|
+
const ctx: RenderCtx = { vars: { ...params }, n: 0 };
|
|
76
|
+
const projection = plan.select && plan.select.length > 0 ? plan.select.join(', ') : '*';
|
|
77
|
+
let sql = `SELECT ${projection} FROM ${plan.table}`;
|
|
78
|
+
if (plan.where && plan.where.length > 0) {
|
|
79
|
+
sql += ` WHERE ${renderWhereSurql(plan.where, ctx)}`;
|
|
80
|
+
}
|
|
81
|
+
if (plan.orderBy && plan.orderBy.length > 0) sql += renderOrderBy(plan.orderBy);
|
|
82
|
+
if (plan.limit !== undefined) sql += ` LIMIT ${plan.limit}`;
|
|
83
|
+
if (plan.offset !== undefined) sql += ` START ${plan.offset}`;
|
|
84
|
+
return { sql: `${sql};`, vars: ctx.vars };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render a batched relation fetch to SurrealQL:
|
|
89
|
+
* `SELECT <select> FROM <table> WHERE <matchField> IN $__keys [AND <where>] [ORDER BY]`.
|
|
90
|
+
* The resolver re-applies per-parent ORDER/LIMIT after grouping, so LIMIT is
|
|
91
|
+
* intentionally omitted here.
|
|
92
|
+
*/
|
|
93
|
+
export function renderRelationFetchSurql(req: RelationFetch): RenderedQuery {
|
|
94
|
+
const ctx: RenderCtx = { vars: { __keys: req.keys }, n: 0 };
|
|
95
|
+
const projection =
|
|
96
|
+
req.select && req.select.length > 0 ? ['id', ...req.select].join(', ') : '*';
|
|
97
|
+
// The correlation keys arrive as record-id STRINGS (`"user:abc"`), but the
|
|
98
|
+
// matched column (`id`, or a `record<…>` foreign key) is a RecordId. In
|
|
99
|
+
// SurrealDB `id IN ["user:abc"]` never matches (string ≠ record), so every
|
|
100
|
+
// `.related()` field would resolve empty. Coerce each key to a record id with
|
|
101
|
+
// `type::record(<string> …)` (idempotent if a key is already a RecordId).
|
|
102
|
+
let sql = `SELECT ${projection} FROM ${req.table} WHERE ${req.matchField} IN $__keys.map(|$__k| type::record(<string> $__k))`;
|
|
103
|
+
if (req.where && req.where.length > 0) {
|
|
104
|
+
sql += ` AND ${renderWhereSurql(req.where, ctx)}`;
|
|
105
|
+
}
|
|
106
|
+
if (req.orderBy && req.orderBy.length > 0) sql += renderOrderBy(req.orderBy);
|
|
107
|
+
return { sql: `${sql};`, vars: ctx.vars };
|
|
108
|
+
}
|