Skip to content

Commit 2c79822

Browse files
committed
Update JSON functions
1 parent d9a8f65 commit 2c79822

20 files changed

+396
-208
lines changed

packages/sync-rules/src/SqlBucketDescriptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class SqlBucketDescriptor implements BucketSource {
6464
if (this.bucketParameters == null) {
6565
throw new Error('Bucket parameters must be defined');
6666
}
67-
const dataRows = SqlDataQuery.fromSql(this.name, this.bucketParameters, sql, options);
67+
const dataRows = SqlDataQuery.fromSql(this.name, this.bucketParameters, sql, options, this.compatibility);
6868

6969
this.dataQueries.push(dataRows);
7070

packages/sync-rules/src/SqlDataQuery.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ import { TablePattern } from './TablePattern.js';
1212
import { TableQuerySchema } from './TableQuerySchema.js';
1313
import { EvaluationResult, ParameterMatchClause, QuerySchema, SqliteRow } from './types.js';
1414
import { getBucketId, isSelectStatement } from './utils.js';
15+
import { CompatibilityContext } from './quirks.js';
1516

1617
export interface SqlDataQueryOptions extends BaseSqlDataQueryOptions {
1718
filter: ParameterMatchClause;
1819
}
1920

2021
export class SqlDataQuery extends BaseSqlDataQuery {
21-
static fromSql(descriptorName: string, bucketParameters: string[], sql: string, options: SyncRulesOptions) {
22+
static fromSql(
23+
descriptorName: string,
24+
bucketParameters: string[],
25+
sql: string,
26+
options: SyncRulesOptions,
27+
compatibility: CompatibilityContext
28+
) {
2229
const parsed = parse(sql, { locationTracking: true });
2330
const schema = options.schema;
2431

@@ -67,6 +74,7 @@ export class SqlDataQuery extends BaseSqlDataQuery {
6774
table: alias,
6875
parameterTables: ['bucket'],
6976
valueTables: [alias],
77+
compatibilityContext: compatibility,
7078
sql,
7179
schema: querySchema
7280
});

packages/sync-rules/src/SqlParameterQuery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
SqliteRow
3030
} from './types.js';
3131
import { filterJsonRow, getBucketId, isJsonValue, isSelectStatement, normalizeParameterValue } from './utils.js';
32+
import { CompatibilityContext } from './quirks.js';
3233

3334
export interface SqlParameterQueryOptions {
3435
sourceTable: TablePattern;
@@ -122,6 +123,7 @@ export class SqlParameterQuery {
122123
sql,
123124
supportsExpandingParameters: true,
124125
supportsParameterExpressions: true,
126+
compatibilityContext: CompatibilityContext.ofFixedQuirks(options.fixedQuirks),
125127
schema: querySchema
126128
});
127129
tools.checkSpecificNameCase(tableRef);

packages/sync-rules/src/SqlSyncRules.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ export class SqlSyncRules implements SyncRules {
194194
const queryOptions: QueryParseOptions = {
195195
...options,
196196
accept_potentially_dangerous_queries,
197-
priority: parseOptionPriority
197+
priority: parseOptionPriority,
198+
fixedQuirks: fixedQuirks
198199
};
199200
const parameters = value.get('parameters', true) as unknown;
200201
const dataQueries = value.get('data', true) as unknown;

packages/sync-rules/src/StaticSqlParameterQuery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SqlTools } from './sql_filters.js';
55
import { checkUnsupportedFeatures, isClauseError, isParameterValueClause, sqliteBool } from './sql_support.js';
66
import { ParameterValueClause, QueryParseOptions, RequestParameters, SqliteJsonValue } from './types.js';
77
import { getBucketId, isJsonValue } from './utils.js';
8+
import { CompatibilityContext } from './quirks.js';
89

910
export interface StaticSqlParameterQueryOptions {
1011
sql: string;
@@ -39,6 +40,7 @@ export class StaticSqlParameterQuery {
3940
table: undefined,
4041
parameterTables: ['token_parameters', 'user_parameters'],
4142
supportsParameterExpressions: true,
43+
compatibilityContext: CompatibilityContext.ofFixedQuirks(options.fixedQuirks),
4244
sql
4345
});
4446
const where = q.where;

packages/sync-rules/src/TableValuedFunctionSqlParameterQuery.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FromCall, SelectedColumn, SelectFromStatement } from 'pgsql-ast-parser'
22
import { SqlRuleError } from './errors.js';
33
import { SqlTools } from './sql_filters.js';
44
import { checkUnsupportedFeatures, isClauseError, isParameterValueClause, sqliteBool } from './sql_support.js';
5-
import { TABLE_VALUED_FUNCTIONS, TableValuedFunction } from './TableValuedFunctions.js';
5+
import { generateTableValuedFunctions, TableValuedFunction } from './TableValuedFunctions.js';
66
import {
77
ParameterValueClause,
88
ParameterValueSet,
@@ -13,6 +13,7 @@ import {
1313
} from './types.js';
1414
import { getBucketId, isJsonValue } from './utils.js';
1515
import { BucketDescription, BucketPriority, DEFAULT_BUCKET_PRIORITY } from './BucketDescription.js';
16+
import { CompatibilityContext } from './quirks.js';
1617

1718
export interface TableValuedFunctionSqlParameterQueryOptions {
1819
sql: string;
@@ -48,11 +49,13 @@ export class TableValuedFunctionSqlParameterQuery {
4849
options: QueryParseOptions,
4950
queryId: string
5051
): TableValuedFunctionSqlParameterQuery {
52+
const compatibility = CompatibilityContext.ofFixedQuirks(options.fixedQuirks);
5153
let errors: SqlRuleError[] = [];
5254

5355
errors.push(...checkUnsupportedFeatures(sql, q));
5456

55-
if (!(call.function.name in TABLE_VALUED_FUNCTIONS)) {
57+
const tableValuedFunctions = generateTableValuedFunctions(compatibility);
58+
if (!(call.function.name in tableValuedFunctions)) {
5659
throw new SqlRuleError(`Table-valued function ${call.function.name} is not defined.`, sql, call);
5760
}
5861

@@ -63,6 +66,7 @@ export class TableValuedFunctionSqlParameterQuery {
6366
table: callTable,
6467
parameterTables: ['token_parameters', 'user_parameters', callTable],
6568
supportsParameterExpressions: true,
69+
compatibilityContext: compatibility,
6670
sql
6771
});
6872
const where = q.where;
@@ -72,7 +76,7 @@ export class TableValuedFunctionSqlParameterQuery {
7276
const columns = q.columns ?? [];
7377
const bucketParameters = columns.map((column) => tools.getOutputName(column));
7478

75-
const functionImpl = TABLE_VALUED_FUNCTIONS[call.function.name]!;
79+
const functionImpl = tableValuedFunctions[call.function.name]!;
7680
let priority = options.priority;
7781
let parameterExtractors: Record<string, ParameterValueClause> = {};
7882

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CompatibilityContext, Quirk } from './quirks.js';
12
import { SqliteJsonValue, SqliteRow, SqliteValue } from './types.js';
23
import { jsonValueToSqlite } from './utils.js';
34

@@ -8,38 +9,40 @@ export interface TableValuedFunction {
89
documentation: string;
910
}
1011

11-
export const JSON_EACH: TableValuedFunction = {
12-
name: 'json_each',
13-
call(args: SqliteValue[]) {
14-
if (args.length != 1) {
15-
throw new Error(`json_each expects 1 argument, got ${args.length}`);
16-
}
17-
const valueString = args[0];
18-
if (valueString === null) {
19-
return [];
20-
} else if (typeof valueString !== 'string') {
21-
throw new Error(`Expected json_each to be called with a string, got ${valueString}`);
22-
}
23-
let values: SqliteJsonValue[] = [];
24-
try {
25-
values = JSON.parse(valueString);
26-
} catch (e) {
27-
throw new Error('Expected JSON string');
28-
}
29-
if (!Array.isArray(values)) {
30-
throw new Error(`Expected an array, got ${valueString}`);
31-
}
12+
function jsonEachImplementation(fixedJsonBehavior: boolean): TableValuedFunction {
13+
return {
14+
name: 'json_each',
15+
call(args: SqliteValue[]) {
16+
if (args.length != 1) {
17+
throw new Error(`json_each expects 1 argument, got ${args.length}`);
18+
}
19+
const valueString = args[0];
20+
if (valueString === null) {
21+
return [];
22+
} else if (typeof valueString !== 'string') {
23+
throw new Error(`Expected json_each to be called with a string, got ${valueString}`);
24+
}
25+
let values: SqliteJsonValue[] = [];
26+
try {
27+
values = JSON.parse(valueString);
28+
} catch (e) {
29+
throw new Error('Expected JSON string');
30+
}
31+
if (!Array.isArray(values)) {
32+
throw new Error(`Expected an array, got ${valueString}`);
33+
}
3234

33-
return values.map((v) => {
34-
return {
35-
value: jsonValueToSqlite(v)
36-
};
37-
});
38-
},
39-
detail: 'Each element of a JSON array',
40-
documentation: 'Returns each element of a JSON array as a separate row.'
41-
};
35+
return values.map((v) => {
36+
return {
37+
value: jsonValueToSqlite(fixedJsonBehavior, v)
38+
};
39+
});
40+
},
41+
detail: 'Each element of a JSON array',
42+
documentation: 'Returns each element of a JSON array as a separate row.'
43+
};
44+
}
4245

43-
export const TABLE_VALUED_FUNCTIONS: Record<string, TableValuedFunction> = {
44-
json_each: JSON_EACH
45-
};
46+
export function generateTableValuedFunctions(compatibility: CompatibilityContext): Record<string, TableValuedFunction> {
47+
return { json_each: jsonEachImplementation(compatibility.isFixed(Quirk.legacyJsonExtract)) };
48+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class SqlEventDescriptor {
2323
}
2424

2525
addSourceQuery(sql: string, options: SyncRulesOptions): QueryParseResult {
26-
const source = SqlEventSourceQuery.fromSql(this.name, sql, options);
26+
const source = SqlEventSourceQuery.fromSql(this.name, sql, options, this.compatibility);
2727

2828
// Each source query should be for a unique table
2929
const existingSourceQuery = this.sourceQueries.find((q) => q.table == source.table);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TablePattern } from '../TablePattern.js';
1010
import { TableQuerySchema } from '../TableQuerySchema.js';
1111
import { EvaluationError, QuerySchema, SqliteJsonRow, SqliteRow } from '../types.js';
1212
import { isSelectStatement } from '../utils.js';
13+
import { CompatibilityContext } from '../quirks.js';
1314

1415
export type EvaluatedEventSourceRow = {
1516
data: SqliteJsonRow;
@@ -24,7 +25,7 @@ export type EvaluatedEventRowWithErrors = {
2425
* Defines how a Replicated Row is mapped to source parameters for events.
2526
*/
2627
export class SqlEventSourceQuery extends BaseSqlDataQuery {
27-
static fromSql(descriptor_name: string, sql: string, options: SyncRulesOptions) {
28+
static fromSql(descriptor_name: string, sql: string, options: SyncRulesOptions, compatibility: CompatibilityContext) {
2829
const parsed = parse(sql, { locationTracking: true });
2930
const schema = options.schema;
3031

@@ -73,7 +74,8 @@ export class SqlEventSourceQuery extends BaseSqlDataQuery {
7374
parameterTables: [],
7475
valueTables: [alias],
7576
sql,
76-
schema: querySchema
77+
schema: querySchema,
78+
compatibilityContext: compatibility
7779
});
7880

7981
let extractors: RowValueExtractor[] = [];

packages/sync-rules/src/quirks.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ export class Quirk {
2626
CompatibilityLevel.SYNC_STREAMS
2727
);
2828

29+
static legacyJsonExtract = new Quirk(
30+
'legacy_json_extract',
31+
"Old versions of the sync service used to treat `->> 'foo.bar'` as a two-element JSON path. With this quirk fixed, it follows modern SQLite and treats it as a single key. The `$.` prefix would be required to split on `.`.",
32+
CompatibilityLevel.SYNC_STREAMS
33+
);
34+
2935
static byName: Record<string, Quirk> = (() => {
3036
const byName: Record<string, Quirk> = {};
31-
for (const entry of [this.nonIso8601Timestamps]) {
37+
for (const entry of [this.nonIso8601Timestamps, this.legacyJsonExtract]) {
3238
byName[entry.name] = entry;
3339
}
3440

0 commit comments

Comments
 (0)