Skip to content

Commit 92cc83b

Browse files
authored
Support SQLite as a scalar expression engine (#654)
1 parent 6e2a57e commit 92cc83b

40 files changed

Lines changed: 737 additions & 346 deletions

.changeset/pretty-ravens-ring.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@powersync/service-core': minor
3+
'@powersync/service-sync-rules': minor
4+
'@powersync/service-module-mongodb-storage': patch
5+
---
6+
7+
Add the experimental `unstable_sqlite_expression_engine` sync config option to evaluate Sync Streams with SQLite.

modules/module-mongodb-storage/src/storage/implementation/MongoPersistedSyncRules.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as sqlite from 'node:sqlite';
2+
13
import { ServiceAssertionError } from '@powersync/lib-services-framework';
24
import { storage } from '@powersync/service-core';
35
import {
@@ -7,6 +9,7 @@ import {
79
DEFAULT_HYDRATION_STATE,
810
HydratedSyncConfig,
911
HydrationState,
12+
nodeSqlite,
1013
ParameterIndexLookupCreator,
1114
ParameterLookupScope,
1215
SyncConfigWithErrors,
@@ -41,7 +44,10 @@ export class MongoPersistedSyncRules implements storage.PersistedSyncRules {
4144
}
4245

4346
hydratedSyncConfig(): HydratedSyncConfig {
44-
return this.syncConfigWithErrors.config.hydrate({ hydrationState: this.hydrationState });
47+
return this.syncConfigWithErrors.config.hydrate({
48+
hydrationState: this.hydrationState,
49+
sqlite: nodeSqlite(sqlite)
50+
});
4551
}
4652
}
4753

modules/module-mongodb-storage/test/src/storage_sync.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { deserializeParameterLookup, JwtPayload, storage, updateSyncRulesFromYaml } from '@powersync/service-core';
22
import { bucketRequest, register, test_utils } from '@powersync/service-core-tests';
3-
import { DEFAULT_HYDRATION_STATE, RequestParameters, SqlSyncRules } from '@powersync/service-sync-rules';
3+
import { DEFAULT_HYDRATION_STATE, nodeSqlite, RequestParameters, SqlSyncRules } from '@powersync/service-sync-rules';
44
import * as bson from 'bson';
5+
import * as sqlite from 'node:sqlite';
56
import { describe, expect, test } from 'vitest';
67
import { MongoBucketStorage } from '../../src/storage/MongoBucketStorage.js';
78
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
@@ -55,7 +56,7 @@ function objectIdGenerator(id: string) {
5556
function hydratedRulesFor(yaml: string) {
5657
const parsed = SqlSyncRules.fromYaml(yaml, test_utils.PARSE_OPTIONS);
5758
expect(parsed.errors).toEqual([]);
58-
return parsed.config.hydrate({ hydrationState: DEFAULT_HYDRATION_STATE });
59+
return parsed.config.hydrate({ hydrationState: DEFAULT_HYDRATION_STATE, sqlite: nodeSqlite(sqlite) });
5960
}
6061

6162
function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, storageVersion: number) {

packages/service-core-tests/src/tests/util.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
ParameterLookupDefinitionId,
55
ParameterLookupScope,
66
SourceTableRef,
7-
SqliteRow,
87
TablePattern
98
} from '@powersync/service-sync-rules';
109
import { bucketRequest } from '../test-utils/general-utils.js';
@@ -36,8 +35,12 @@ const EMPTY_LOOKUP_SOURCE: ParameterIndexLookupCreator = {
3635
getSourceTables(): Set<TablePattern> {
3736
return new Set();
3837
},
39-
evaluateParameterRow(_sourceTable: SourceTableRef, _row: SqliteRow) {
40-
return [];
38+
createEvaluator(input) {
39+
return {
40+
evaluateParameterRow(sourceTable, row) {
41+
return [];
42+
}
43+
};
4144
},
4245
tableSyncsParameters(_table: SourceTableRef): boolean {
4346
return false;

packages/service-core/src/routes/endpoints/admin.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import * as sqlite from 'node:sqlite';
2+
13
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
2-
import { SourceSchema, SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
4+
import { nodeSqlite, SourceSchema, SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
35
import { internal_routes } from '@powersync/service-types';
46

57
import { DEFAULT_HYDRATION_STATE } from '@powersync/service-sync-rules';
@@ -182,7 +184,10 @@ class FakeSyncRulesContentForValidation extends storage.PersistedSyncRulesConten
182184
}),
183185
hydrationState: DEFAULT_HYDRATION_STATE,
184186
hydratedSyncConfig() {
185-
return this.syncConfigWithErrors.config.hydrate({ hydrationState: DEFAULT_HYDRATION_STATE });
187+
return this.syncConfigWithErrors.config.hydrate({
188+
hydrationState: DEFAULT_HYDRATION_STATE,
189+
sqlite: nodeSqlite(sqlite)
190+
});
186191
}
187192
};
188193
}

packages/service-core/src/storage/PersistedSyncRulesContent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import {
77
ErrorLocation,
88
HydratedSyncConfig,
99
HydrationState,
10-
javaScriptExpressionEngine,
10+
nodeSqlite,
1111
PrecompiledSyncConfig,
1212
SqlEventDescriptor,
1313
SqlSyncRules,
1414
SyncConfigWithErrors,
1515
versionedHydrationState,
1616
YamlError
1717
} from '@powersync/service-sync-rules';
18+
import * as sqlite from 'node:sqlite';
1819
import { SerializedSyncPlan, UpdateSyncRulesOptions } from './BucketStorageFactory.js';
1920
import { ReplicationLock } from './ReplicationLock.js';
2021
import { STORAGE_VERSION_CONFIG, StorageVersionConfig } from './StorageVersionConfig.js';
@@ -101,7 +102,6 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
101102

102103
const precompiled = new PrecompiledSyncConfig(plan, compatibility, eventDefinitions, {
103104
defaultSchema: options.defaultSchema,
104-
engine: javaScriptExpressionEngine(compatibility),
105105
sourceText: this.sync_rules_content
106106
});
107107

@@ -144,7 +144,7 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
144144
syncConfigWithErrors: config,
145145
hydrationState,
146146
hydratedSyncConfig: () => {
147-
return config.config.hydrate({ hydrationState });
147+
return config.config.hydrate({ hydrationState, sqlite: nodeSqlite(sqlite) });
148148
}
149149
};
150150
}

packages/service-core/test/src/routes/stream.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { BasicRouterRequest, Context, JwtPayload, SyncRulesBucketStorage } from '@/index.js';
2-
import { RouterResponse, ServiceError, logger } from '@powersync/lib-services-framework';
3-
import { DEFAULT_HYDRATION_STATE, SqlSyncRules } from '@powersync/service-sync-rules';
2+
import { logger, RouterResponse, ServiceError } from '@powersync/lib-services-framework';
3+
import {
4+
DEFAULT_HYDRATION_STATE,
5+
HydrateSyncConfigParams,
6+
nodeSqlite,
7+
SqlSyncRules
8+
} from '@powersync/service-sync-rules';
9+
import * as sqlite from 'node:sqlite';
410
import { Readable, Writable } from 'stream';
511
import { pipeline } from 'stream/promises';
612
import { describe, expect, it } from 'vitest';
@@ -10,6 +16,11 @@ import { DEFAULT_PARAM_LOGGING_FORMAT_OPTIONS, limitParamsForLogging } from '../
1016
import { mockServiceContext } from './mocks.js';
1117

1218
describe('Stream Route', () => {
19+
const defaultHydrationOptions: HydrateSyncConfigParams = {
20+
hydrationState: DEFAULT_HYDRATION_STATE,
21+
sqlite: nodeSqlite(sqlite)
22+
};
23+
1324
describe('compressed stream', () => {
1425
it('handles missing sync rules', async () => {
1526
const context: Context = {
@@ -45,7 +56,7 @@ describe('Stream Route', () => {
4556

4657
const storage = {
4758
getParsedSyncRules() {
48-
return new SqlSyncRules('bucket_definitions: {}').hydrate({ hydrationState: DEFAULT_HYDRATION_STATE });
59+
return new SqlSyncRules('bucket_definitions: {}').hydrate(defaultHydrationOptions);
4960
},
5061
watchCheckpointChanges: async function* (options) {
5162
throw new Error('Simulated storage error');
@@ -83,7 +94,7 @@ describe('Stream Route', () => {
8394
it('logs the application metadata', async () => {
8495
const storage = {
8596
getParsedSyncRules() {
86-
return new SqlSyncRules('bucket_definitions: {}').hydrate({ hydrationState: DEFAULT_HYDRATION_STATE });
97+
return new SqlSyncRules('bucket_definitions: {}').hydrate(defaultHydrationOptions);
8798
},
8899
watchCheckpointChanges: async function* (options) {
89100
throw new Error('Simulated storage error');

packages/service-core/test/src/sync/BucketChecksumState.test.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ import {
1515
} from '@/index.js';
1616
import { JSONBig } from '@powersync/service-jsonbig';
1717
import {
18+
nodeSqlite,
1819
ParameterIndexLookupCreator,
1920
ParameterLookupDefinitionId,
2021
ParameterLookupRows,
2122
ParameterLookupScope,
2223
ScopedParameterLookup,
2324
SourceTableRef,
24-
SqliteRow,
2525
SqlSyncRules,
2626
TablePattern,
2727
versionedHydrationState
2828
} from '@powersync/service-sync-rules';
29+
import * as sqlite from 'node:sqlite';
2930
import { beforeEach, describe, expect, test } from 'vitest';
3031

3132
describe('BucketChecksumState', () => {
@@ -39,8 +40,12 @@ describe('BucketChecksumState', () => {
3940
getSourceTables(): Set<TablePattern> {
4041
return new Set();
4142
},
42-
evaluateParameterRow(_sourceTable: SourceTableRef, _row: SqliteRow) {
43-
return [];
43+
createEvaluator() {
44+
return {
45+
evaluateParameterRow() {
46+
return [];
47+
}
48+
};
4449
},
4550
tableSyncsParameters(_table: SourceTableRef): boolean {
4651
return false;
@@ -60,7 +65,7 @@ bucket_definitions:
6065
data: []
6166
`,
6267
{ defaultSchema: 'public' }
63-
).config.hydrate({ hydrationState: versionedHydrationState(1) });
68+
).config.hydrate({ hydrationState: versionedHydrationState(1), sqlite: nodeSqlite(sqlite) });
6469

6570
// global[1] and global[2]
6671
const SYNC_RULES_GLOBAL_TWO = SqlSyncRules.fromYaml(
@@ -73,7 +78,7 @@ bucket_definitions:
7378
data: []
7479
`,
7580
{ defaultSchema: 'public' }
76-
).config.hydrate({ hydrationState: versionedHydrationState(2) });
81+
).config.hydrate({ hydrationState: versionedHydrationState(2), sqlite: nodeSqlite(sqlite) });
7782

7883
// by_project[n]
7984
const SYNC_RULES_DYNAMIC = SqlSyncRules.fromYaml(
@@ -84,7 +89,7 @@ bucket_definitions:
8489
data: []
8590
`,
8691
{ defaultSchema: 'public' }
87-
).config.hydrate({ hydrationState: versionedHydrationState(3) });
92+
).config.hydrate({ hydrationState: versionedHydrationState(3), sqlite: nodeSqlite(sqlite) });
8893

8994
const syncContext = new SyncContext({
9095
maxBuckets: 100,
@@ -655,7 +660,7 @@ config:
655660

656661
const rules = SqlSyncRules.fromYaml(source, {
657662
defaultSchema: 'public'
658-
}).config.hydrate({ hydrationState: versionedHydrationState(1) });
663+
}).config.hydrate({ hydrationState: versionedHydrationState(1), sqlite: nodeSqlite(sqlite) });
659664

660665
return new BucketChecksumState({
661666
syncContext,
@@ -921,7 +926,8 @@ streams:
921926
`,
922927
{ defaultSchema: 'public' }
923928
).config.hydrate({
924-
hydrationState: versionedHydrationState(1)
929+
hydrationState: versionedHydrationState(1),
930+
sqlite: nodeSqlite(sqlite)
925931
});
926932

927933
const storage = new MockBucketChecksumStateStorage();
@@ -1018,7 +1024,8 @@ streams:
10181024
`,
10191025
{ defaultSchema: 'public' }
10201026
).config.hydrate({
1021-
hydrationState: versionedHydrationState(1)
1027+
hydrationState: versionedHydrationState(1),
1028+
sqlite: nodeSqlite(sqlite)
10221029
});
10231030

10241031
const storage = new MockBucketChecksumStateStorage();
@@ -1090,7 +1097,7 @@ streams:
10901097
query: SELECT id FROM comments WHERE p IN auth.parameter('c')
10911098
`,
10921099
{ defaultSchema: 'public' }
1093-
).config.hydrate({ hydrationState: versionedHydrationState(4) });
1100+
).config.hydrate({ hydrationState: versionedHydrationState(4), sqlite: nodeSqlite(sqlite) });
10941101

10951102
const storage = new MockBucketChecksumStateStorage();
10961103

@@ -1165,7 +1172,8 @@ streams:
11651172
}
11661173

11671174
const SYNC_RULES_MANY = SqlSyncRules.fromYaml(yamlDefinitions, { defaultSchema: 'public' }).config.hydrate({
1168-
hydrationState: versionedHydrationState(5)
1175+
hydrationState: versionedHydrationState(5),
1176+
sqlite: nodeSqlite(sqlite)
11691177
});
11701178

11711179
const storage = new MockBucketChecksumStateStorage();

0 commit comments

Comments
 (0)