Skip to content

Commit 32c548a

Browse files
committed
Introduce compatibility edition
1 parent d9a8f65 commit 32c548a

17 files changed

+185
-126
lines changed

packages/sync-rules/src/SqlBucketDescriptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { StaticSqlParameterQuery } from './StaticSqlParameterQuery.js';
1111
import { TablePattern } from './TablePattern.js';
1212
import { TableValuedFunctionSqlParameterQuery } from './TableValuedFunctionSqlParameterQuery.js';
1313
import { SqlRuleError } from './errors.js';
14-
import { CompatibilityContext, Quirk } from './quirks.js';
14+
import { CompatibilityContext } from './compatibility.js';
1515
import {
1616
EvaluatedParametersResult,
1717
EvaluateRowOptions,

packages/sync-rules/src/SqlSyncRules.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ import {
2222
SourceSchema,
2323
SqliteInputRow,
2424
SqliteJsonRow,
25-
SqliteRow,
2625
StreamParseOptions,
2726
SyncRules
2827
} from './types.js';
2928
import { BucketSource } from './BucketSource.js';
30-
import { SyncStream } from './streams/stream.js';
3129
import { syncStreamFromSql } from './streams/from_sql.js';
32-
import { CompatibilityContext, Quirk } from './quirks.js';
30+
import { CompatibilityContext, CompatibilityEdition, CompatibilityOption } from './compatibility.js';
3331

3432
const ACCEPT_POTENTIALLY_DANGEROUS_QUERIES = Symbol('ACCEPT_POTENTIALLY_DANGEROUS_QUERIES');
3533

@@ -140,16 +138,25 @@ export class SqlSyncRules implements SyncRules {
140138
return rules;
141139
}
142140

143-
const rawFixedQuirks = parsed.get('fixed_quirks') as YAMLSeq<Scalar> | null;
144-
const fixedQuirks: Quirk[] = [];
145-
if (rawFixedQuirks != null) {
146-
for (const entry of rawFixedQuirks.items) {
147-
const quirk = Quirk.byName[entry.value as string];
148-
if (quirk != null) {
149-
fixedQuirks.push(quirk);
141+
const declaredOptions = parsed.get('config') as YAMLMap | null;
142+
let compatibility = CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY;
143+
if (declaredOptions != null) {
144+
const edition = (declaredOptions.get('edition') ?? CompatibilityEdition.LEGACY) as CompatibilityEdition;
145+
const options = new Map<CompatibilityOption, boolean>();
146+
147+
for (const entry of declaredOptions.items) {
148+
const {
149+
key: { value: key },
150+
value: { value }
151+
} = entry as { key: Scalar<string>; value: Scalar<boolean> };
152+
153+
const option = CompatibilityOption.byName[key];
154+
if (option) {
155+
options.set(option, value);
150156
}
151-
// Note: We don't need a custom warning message for unknown names here, the schema will reject those values.
152157
}
158+
159+
compatibility = new CompatibilityContext(edition, options);
153160
}
154161

155162
// Bucket definitions using explicit parameter and data queries.
@@ -194,12 +201,13 @@ export class SqlSyncRules implements SyncRules {
194201
const queryOptions: QueryParseOptions = {
195202
...options,
196203
accept_potentially_dangerous_queries,
197-
priority: parseOptionPriority
204+
priority: parseOptionPriority,
205+
compatibility
198206
};
199207
const parameters = value.get('parameters', true) as unknown;
200208
const dataQueries = value.get('data', true) as unknown;
201209

202-
const descriptor = new SqlBucketDescriptor(key, CompatibilityContext.ofFixedQuirks(fixedQuirks));
210+
const descriptor = new SqlBucketDescriptor(key, compatibility);
203211

204212
if (parameters instanceof Scalar) {
205213
rules.withScalar(parameters, (q) => {
@@ -242,7 +250,7 @@ export class SqlSyncRules implements SyncRules {
242250
accept_potentially_dangerous_queries,
243251
priority: rules.parsePriority(value),
244252
auto_subscribe: value.get('auto_subscribe', true)?.value == true,
245-
fixedQuirks
253+
compatibility
246254
};
247255

248256
const data = value.get('query', true) as unknown;
@@ -276,7 +284,7 @@ export class SqlSyncRules implements SyncRules {
276284
continue;
277285
}
278286

279-
const eventDescriptor = new SqlEventDescriptor(key.toString(), CompatibilityContext.ofFixedQuirks(fixedQuirks));
287+
const eventDescriptor = new SqlEventDescriptor(key.toString(), compatibility);
280288
for (let item of payloads.items) {
281289
if (!isScalar(item)) {
282290
rules.errors.push(new YamlError(new Error(`Payload queries for events must be scalar.`)));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export enum CompatibilityEdition {
2+
LEGACY = 1,
3+
SYNC_STREAMS = 2
4+
}
5+
6+
/**
7+
* A historical issue of the PowerSync service that can only be changed in a backwards-incompatible manner.
8+
*
9+
* To avoid breaking existing users, fixes to those quirks are opt-in: Users either have to use `fixed_quirks` list when
10+
* defining sync rules or use a new feature such as sync streams where these issues are fixed by default.
11+
*/
12+
export class CompatibilityOption {
13+
private constructor(
14+
readonly name: string,
15+
readonly description: string,
16+
readonly fixedIn: CompatibilityEdition
17+
) {}
18+
19+
static timestampsIso8601 = new CompatibilityOption(
20+
'timestamps_iso8601',
21+
'Consistently renders timestamps with an ISO 8601-compatible format (previous versions used a space instead of a T to separate date and time).',
22+
CompatibilityEdition.SYNC_STREAMS
23+
);
24+
25+
static byName: Record<string, CompatibilityOption> = Object.freeze({
26+
timestamps_iso8601: this.timestampsIso8601
27+
});
28+
}
29+
30+
export class CompatibilityContext {
31+
/**
32+
* The general compatibility level we're operating under.
33+
*
34+
* This is {@link CompatibilityEdition.LEGACY} by default, but can be changed when defining sync rules to allow newer
35+
* features.
36+
*/
37+
readonly edition: CompatibilityEdition;
38+
39+
/**
40+
* Overrides to customize used compatibility options to deviate from defaults at the given {@link edition}.
41+
*/
42+
readonly overrides: Map<CompatibilityOption, boolean>;
43+
44+
constructor(edition: CompatibilityEdition, overrides?: Map<CompatibilityOption, boolean>) {
45+
this.edition = edition;
46+
this.overrides = overrides ?? new Map();
47+
}
48+
49+
isEnabled(option: CompatibilityOption) {
50+
return this.overrides.get(option) ?? option.fixedIn <= this.edition;
51+
}
52+
53+
/**
54+
* A {@link CompatibilityContext} in which no fixes are applied.
55+
*/
56+
static FULL_BACKWARDS_COMPATIBILITY: CompatibilityContext = new CompatibilityContext(CompatibilityEdition.LEGACY);
57+
}

packages/sync-rules/src/events/SqlEventDescriptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SqlRuleError } from '../errors.js';
2-
import { CompatibilityContext } from '../quirks.js';
2+
import { CompatibilityContext } from '../compatibility.js';
33
import { SourceTableInterface } from '../SourceTableInterface.js';
44
import { QueryParseResult } from '../SqlBucketDescriptor.js';
55
import { SyncRulesOptions } from '../SqlSyncRules.js';

packages/sync-rules/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
export * from './BucketDescription.js';
22
export * from './BucketParameterQuerier.js';
33
export * from './BucketSource.js';
4+
export * from './compatibility.js';
45
export * from './errors.js';
56
export * from './events/SqlEventDescriptor.js';
67
export * from './events/SqlEventSourceQuery.js';
78
export * from './ExpressionType.js';
89
export * from './IdSequence.js';
910
export * from './json_schema.js';
10-
export * from './quirks.js';
1111
export * from './request_functions.js';
1212
export * from './schema-generators/schema-generators.js';
1313
export * from './SourceTableInterface.js';

packages/sync-rules/src/json_schema.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ajvModule from 'ajv';
2-
import { Quirk } from './quirks.js';
2+
import { CompatibilityEdition, CompatibilityOption } from './compatibility.js';
33
// Hack to make this work both in NodeJS and a browser
44
const Ajv = ajvModule.default ?? ajvModule;
55
const ajv = new Ajv({ allErrors: true, verbose: true });
@@ -109,12 +109,29 @@ export const syncRulesSchema: ajvModule.Schema = {
109109
}
110110
}
111111
},
112-
fixed_quirks: {
113-
type: 'array',
114-
description: 'Opt-in to backwards-incompatible fixes of historical quirks and issues of the sync service.',
115-
items: {
116-
enum: Object.keys(Quirk.byName)
117-
}
112+
config: {
113+
type: 'object',
114+
description: 'Config declaring the compatibility level used to parse these definitions.',
115+
properties: {
116+
edition: {
117+
type: 'integer',
118+
default: CompatibilityEdition.LEGACY,
119+
minimum: CompatibilityEdition.LEGACY,
120+
exclusiveMaximum: CompatibilityEdition.SYNC_STREAMS + 1
121+
},
122+
...Object.fromEntries(
123+
Object.entries(CompatibilityOption.byName).map((e) => {
124+
return [
125+
e[0],
126+
{
127+
type: 'boolean',
128+
description: `Enabled by default starting from edition ${e[1].fixedIn}: ${e[1].description}`
129+
}
130+
];
131+
})
132+
)
133+
},
134+
additionalProperties: false
118135
}
119136
},
120137
anyOf: [{ required: ['bucket_definitions'] }, { required: ['streams'] }],

packages/sync-rules/src/quirks.ts

Lines changed: 0 additions & 73 deletions
This file was deleted.

packages/sync-rules/src/streams/from_sql.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
Statement
4242
} from 'pgsql-ast-parser';
4343
import { STREAM_FUNCTIONS } from './functions.js';
44-
import { CompatibilityContext, CompatibilityLevel } from '../quirks.js';
44+
import { CompatibilityContext, CompatibilityEdition } from '../compatibility.js';
4545

4646
export function syncStreamFromSql(
4747
descriptorName: string,
@@ -67,6 +67,13 @@ class SyncStreamCompiler {
6767
}
6868

6969
compile(): SyncStream {
70+
if (this.options.compatibility.edition < CompatibilityEdition.SYNC_STREAMS) {
71+
throw new SqlRuleError(
72+
'Sync streams require edition 2 or later. Try adding a `config: {edition: 2} block to the end of the file.`',
73+
this.sql
74+
);
75+
}
76+
7077
const [stmt, ...illegalRest] = parse(this.sql, { locationTracking: true });
7178

7279
// TODO: Share more of this code with SqlDataQuery
@@ -93,7 +100,7 @@ class SyncStreamCompiler {
93100
const stream = new SyncStream(
94101
this.descriptorName,
95102
new BaseSqlDataQuery(this.compileDataQuery(tools, query, alias, sourceTable)),
96-
new CompatibilityContext(CompatibilityLevel.SYNC_STREAMS, this.options.fixedQuirks)
103+
this.options.compatibility
97104
);
98105
stream.subscribedToByDefault = this.options.auto_subscribe ?? false;
99106
if (filter.isValid(tools)) {

packages/sync-rules/src/streams/stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BucketInclusionReason, BucketPriority, DEFAULT_BUCKET_PRIORITY } from '
33
import { BucketParameterQuerier, PendingQueriers } from '../BucketParameterQuerier.js';
44
import { BucketSource, BucketSourceType, ResultSetDescription } from '../BucketSource.js';
55
import { ColumnDefinition } from '../ExpressionType.js';
6-
import { CompatibilityContext } from '../quirks.js';
6+
import { CompatibilityContext } from '../compatibility.js';
77
import { SourceTableInterface } from '../SourceTableInterface.js';
88
import { GetQuerierOptions, RequestedStream } from '../SqlSyncRules.js';
99
import { TablePattern } from '../TablePattern.js';

packages/sync-rules/src/types.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { TablePattern } from './TablePattern.js';
66
import { toSyncRulesParameters } from './utils.js';
77
import { BucketPriority } from './BucketDescription.js';
88
import { ParameterLookup } from './BucketParameterQuerier.js';
9-
import { DateTimeValue } from './types/time.js';
109
import { CustomSqliteValue } from './types/custom_sqlite_value.js';
11-
import { CompatibilityContext, Quirk } from './quirks.js';
10+
import { CompatibilityContext } from './compatibility.js';
1211

1312
export interface SyncRules {
1413
evaluateRow(options: EvaluateRowOptions): EvaluationResult[];
@@ -19,11 +18,11 @@ export interface SyncRules {
1918
export interface QueryParseOptions extends SyncRulesOptions {
2019
accept_potentially_dangerous_queries?: boolean;
2120
priority?: BucketPriority;
21+
compatibility: CompatibilityContext;
2222
}
2323

2424
export interface StreamParseOptions extends QueryParseOptions {
2525
auto_subscribe?: boolean;
26-
fixedQuirks: Quirk[];
2726
}
2827

2928
export interface EvaluatedParameters {

0 commit comments

Comments
 (0)