Skip to content

Commit af096ea

Browse files
committed
Merge remote-tracking branch 'origin/main' into granular-sync-rules
2 parents f919d20 + c2da2be commit af096ea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1194
-274
lines changed

.changeset/nasty-snakes-double.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@powersync/service-core': patch
3+
'@powersync/service-image': patch
4+
---
5+
6+
- Rework dynamic module loading, fixing startup issues for migration and compact jobs in 1.18.0 / 1.18.1
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@powersync/service-image': minor
3+
'@powersync/service-module-postgres': patch
4+
'@powersync/service-module-mongodb': patch
5+
'@powersync/service-module-mysql': patch
6+
'@powersync/service-module-mssql': patch
7+
'@powersync/service-sync-rules': patch
8+
'@powersync/service-jpgwire': patch
9+
---
10+
11+
Add the `timestamp_max_precision` option for sync rules. It can be set to `seconds`, `milliseconds` or `microseconds` to restrict the precision of synced datetime values.

.changeset/ten-walls-cough.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@powersync/service-module-postgres': patch
3+
'@powersync/service-jpgwire': patch
4+
---
5+
6+
Update `pgwire` to version `0.8.1`.

libs/lib-postgres/src/db/connection/AbstractPostgresConnection.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,11 @@ export abstract class AbstractPostgresConnection<Listener = {}> extends framewor
6969
}
7070

7171
async *streamRows<T>(...args: pgwire.Statement[]): AsyncIterableIterator<T[]> {
72-
let columns: Array<keyof T> = [];
72+
let columns: Array<pgwire.ColumnDescription> = [];
7373

7474
for await (const chunk of this.stream(...args)) {
7575
if (chunk.tag == 'RowDescription') {
76-
columns = chunk.payload.map((c, index) => {
77-
return c.name as keyof T;
78-
});
76+
columns = chunk.payload;
7977
continue;
8078
}
8179

@@ -86,7 +84,7 @@ export abstract class AbstractPostgresConnection<Listener = {}> extends framewor
8684
yield chunk.rows.map((row) => {
8785
let q: Partial<T> = {};
8886
for (const [index, c] of columns.entries()) {
89-
q[c] = row[index];
87+
q[c.name as keyof T] = row.decodeWithoutCustomTypes(index);
9088
}
9189
return q as T;
9290
});

modules/module-mongodb/src/replication/MongoRelation.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
CustomSqliteValue,
99
SqliteInputRow,
1010
SqliteInputValue,
11-
DateTimeValue
11+
DateTimeValue,
12+
DateTimeSourceOptions,
13+
TimeValuePrecision
1214
} from '@powersync/service-sync-rules';
1315

1416
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
@@ -69,7 +71,7 @@ export function toMongoSyncRulesValue(data: any): SqliteInputValue {
6971
return data.toHexString();
7072
} else if (data instanceof Date) {
7173
const isoString = data.toISOString();
72-
return new DateTimeValue(isoString);
74+
return new DateTimeValue(isoString, undefined, mongoTimeOptions);
7375
} else if (data instanceof mongo.Binary) {
7476
return new Uint8Array(data.buffer);
7577
} else if (data instanceof mongo.Long) {
@@ -122,7 +124,7 @@ function filterJsonData(data: any, context: CompatibilityContext, depth = 0): an
122124
return data;
123125
} else if (data instanceof Date) {
124126
const isoString = data.toISOString();
125-
return new DateTimeValue(isoString).toSqliteValue(context);
127+
return new DateTimeValue(isoString, undefined, mongoTimeOptions).toSqliteValue(context);
126128
} else if (data instanceof mongo.ObjectId) {
127129
return data.toHexString();
128130
} else if (data instanceof mongo.UUID) {
@@ -195,3 +197,8 @@ export async function createCheckpoint(
195197
await session.endSession();
196198
}
197199
}
200+
201+
const mongoTimeOptions: DateTimeSourceOptions = {
202+
subSecondPrecision: TimeValuePrecision.milliseconds,
203+
defaultSubSecondPrecision: TimeValuePrecision.milliseconds
204+
};

modules/module-mongodb/test/src/mongo_test.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
CompatibilityContext,
55
CompatibilityEdition,
66
SqliteInputRow,
7-
SqlSyncRules
7+
SqlSyncRules,
8+
TimeValuePrecision
89
} from '@powersync/service-sync-rules';
910
import { describe, expect, test } from 'vitest';
1011

@@ -555,11 +556,23 @@ bucket_definitions:
555556
noFraction: '2023-03-06 13:47:01.000Z'
556557
});
557558

558-
const newFormat = applyRowContext(row, new CompatibilityContext(CompatibilityEdition.SYNC_STREAMS));
559+
const newFormat = applyRowContext(row, new CompatibilityContext({ edition: CompatibilityEdition.SYNC_STREAMS }));
559560
expect(newFormat).toMatchObject({
560561
fraction: '2023-03-06T13:47:01.123Z',
561562
noFraction: '2023-03-06T13:47:01.000Z'
562563
});
564+
565+
const reducedPrecisionFormat = applyRowContext(
566+
row,
567+
new CompatibilityContext({
568+
edition: CompatibilityEdition.SYNC_STREAMS,
569+
maxTimeValuePrecision: TimeValuePrecision.seconds
570+
})
571+
);
572+
expect(reducedPrecisionFormat).toMatchObject({
573+
fraction: '2023-03-06T13:47:01Z',
574+
noFraction: '2023-03-06T13:47:01Z'
575+
});
563576
} finally {
564577
await client.close();
565578
}

modules/module-mssql/src/common/mssqls-to-sqlite.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import sql from 'mssql';
22
import {
33
DatabaseInputRow,
4+
DateTimeSourceOptions,
5+
DateTimeValue,
46
ExpressionType,
57
SQLITE_FALSE,
68
SQLITE_TRUE,
79
SqliteInputRow,
10+
TimeValue,
11+
TimeValuePrecision,
812
toSyncRulesRow
913
} from '@powersync/service-sync-rules';
1014
import { MSSQLUserDefinedType } from '../types/mssql-data-types.js';
@@ -32,14 +36,25 @@ export function toSqliteInputRow(row: any, columns: sql.IColumnMetadata): Sqlite
3236
result[key] = toISODateString(row[key] as Date);
3337
break;
3438
case sql.TYPES.Time:
35-
result[key] = toISOTimeString(row[key] as Date);
39+
result[key] = toISOTimeValue(row[key] as DateWithNanosecondsDelta);
3640
break;
3741
case sql.TYPES.DateTime:
3842
case sql.TYPES.DateTime2:
3943
case sql.TYPES.SmallDateTime:
4044
case sql.TYPES.DateTimeOffset: // The offset is lost when the driver converts to Date. This needs to be handled in the sql query.
41-
const date = row[key] as Date;
42-
result[key] = isNaN(date.getTime()) ? null : date.toISOString();
45+
const date = row[key] as DateWithNanosecondsDelta;
46+
if (isNaN(date.getTime())) {
47+
result[key] = null;
48+
break;
49+
}
50+
51+
const originalFormat = date.toISOString();
52+
const withNanos =
53+
originalFormat.substring(0, originalFormat.length - 1) + // Drop the trailing Z
54+
subMillisecondSuffix(date) + // Expand the .xyz millisecond part to include nanoseconds
55+
'Z';
56+
57+
result[key] = new DateTimeValue(withNanos, withNanos, msSqlTimeSourceOptions);
4358
break;
4459
case sql.TYPES.Binary:
4560
case sql.TYPES.VarBinary:
@@ -74,13 +89,33 @@ function toISODateString(date: Date): string | null {
7489
return isNaN(date.getTime()) ? null : date.toISOString().split('T')[0];
7590
}
7691

92+
// https://github.com/tediousjs/tedious/blob/0c256f186600d7230aec05553ebad209bed81acc/src/value-parser.ts#L668-L670
93+
interface DateWithNanosecondsDelta extends Date {
94+
nanosecondsDelta?: number;
95+
}
96+
7797
/**
7898
* MSSQL time format is HH:mm:ss[.nnnnnnn]
79-
* @param date
80-
* @returns
8199
*/
82-
function toISOTimeString(date: Date): string | null {
83-
return isNaN(date.getTime()) ? null : date.toISOString().split('T')[1].replace('Z', '');
100+
function toISOTimeValue(date: DateWithNanosecondsDelta): TimeValue | null {
101+
if (isNaN(date.getTime())) {
102+
return null;
103+
}
104+
105+
const withMillis = date.toISOString().split('T')[1].replace('Z', '') + subMillisecondSuffix(date);
106+
return TimeValue.parse(withMillis, msSqlTimeSourceOptions);
107+
}
108+
109+
function subMillisecondSuffix(value: DateWithNanosecondsDelta): string {
110+
if (value.nanosecondsDelta != null) {
111+
// nanosecondsDelta is actually measured in seconds. It will always be smaller than 1ms though, because that delta
112+
// is stored on the datetime itself. We truncate here as a precaution.
113+
const actualNanos = Math.round(value.nanosecondsDelta * Math.pow(10, 9)) % Math.pow(10, 6);
114+
115+
return actualNanos.toString().padStart(6, '0');
116+
} else {
117+
return '000000';
118+
}
84119
}
85120

86121
/**
@@ -156,3 +191,9 @@ export function CDCToSqliteRow(options: CDCRowToSqliteRowOptions): SqliteInputRo
156191
}
157192
return toSqliteInputRow(filteredRow, columns);
158193
}
194+
195+
const msSqlTimeSourceOptions: DateTimeSourceOptions = {
196+
// We actually only have 100ns precision (7 digits), but all the options are SI prefixes.
197+
subSecondPrecision: TimeValuePrecision.nanoseconds,
198+
defaultSubSecondPrecision: TimeValuePrecision.nanoseconds
199+
};

modules/module-mssql/test/src/mssql-to-sqlite.test.ts

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { SQLITE_TRUE, SqliteInputRow } from '@powersync/service-sync-rules';
1+
import {
2+
applyRowContext,
3+
CompatibilityContext,
4+
SQLITE_TRUE,
5+
SqliteInputRow,
6+
TimeValuePrecision
7+
} from '@powersync/service-sync-rules';
28
import { afterAll, beforeEach, describe, expect, test } from 'vitest';
39
import { clearTestDb, createUpperCaseUUID, TEST_CONNECTION_OPTIONS, waitForPendingCDCChanges } from './util.js';
410
import { CDCToSqliteRow, toSqliteInputRow } from '@module/common/mssqls-to-sqlite.js';
@@ -43,10 +49,10 @@ describe('MSSQL Data Types Tests', () => {
4349
4450
date_col DATE,
4551
datetime_col DATETIME,
46-
datetime2_col DATETIME2(6),
52+
datetime2_col DATETIME2(7),
4753
smalldatetime_col SMALLDATETIME,
4854
datetimeoffset_col DATETIMEOFFSET(3),
49-
time_col TIME(6),
55+
time_col TIME(7),
5056
5157
char_col CHAR(10),
5258
varchar_col VARCHAR(255),
@@ -214,7 +220,14 @@ describe('MSSQL Data Types Tests', () => {
214220

215221
test('Date types mappings', async () => {
216222
const beforeLSN = await getLatestLSN(connectionManager);
217-
const testDate = new Date('2023-03-06T15:47:00.000Z');
223+
const testDate = new Date('2023-03-06T15:47:00.123Z');
224+
// This adds 0.4567 milliseconds to the JS date, see https://github.com/tediousjs/tedious/blob/0c256f186600d7230aec05553ebad209bed81acc/src/data-types/datetime2.ts#L74.
225+
// Note that there's a typo in tedious there. When reading dates, the property is actually called nanosecondsDelta.
226+
// This is only relevant when binding datetime values, so only in this test.
227+
Object.defineProperty(testDate, 'nanosecondDelta', {
228+
enumerable: false,
229+
value: 0.0004567
230+
});
218231
await connectionManager.query(
219232
`
220233
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(
@@ -235,9 +248,9 @@ describe('MSSQL Data Types Tests', () => {
235248
[
236249
{ name: 'date_col', type: sql.Date, value: testDate },
237250
{ name: 'datetime_col', type: sql.DateTime, value: testDate },
238-
{ name: 'datetime2_col', type: sql.DateTime2(6), value: testDate },
251+
{ name: 'datetime2_col', type: sql.DateTime2(7), value: testDate },
239252
{ name: 'smalldatetime_col', type: sql.SmallDateTime, value: testDate },
240-
{ name: 'time_col', type: sql.Time(6), value: testDate }
253+
{ name: 'time_col', type: sql.Time(7), value: testDate }
241254
]
242255
);
243256
await waitForPendingCDCChanges(beforeLSN, connectionManager);
@@ -246,14 +259,32 @@ describe('MSSQL Data Types Tests', () => {
246259
const replicatedRows = await getReplicatedRows(connectionManager, 'test_data');
247260
const expectedResult = {
248261
date_col: '2023-03-06',
249-
datetime_col: '2023-03-06T15:47:00.000Z',
250-
datetime2_col: '2023-03-06T15:47:00.000Z',
251-
smalldatetime_col: '2023-03-06T15:47:00.000Z',
252-
time_col: '15:47:00.000'
262+
datetime_col: '2023-03-06T15:47:00.123000000Z',
263+
datetime2_col: '2023-03-06T15:47:00.123456700Z',
264+
smalldatetime_col: '2023-03-06T15:47:00.000000000Z',
265+
time_col: '15:47:00.123456700'
253266
};
254267

255-
expect(databaseRows[0]).toMatchObject(expectedResult);
256-
expect(replicatedRows[0]).toMatchObject(expectedResult);
268+
expect(applyRowContext(databaseRows[0], CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY)).toMatchObject(
269+
expectedResult
270+
);
271+
expect(applyRowContext(replicatedRows[0], CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY)).toMatchObject(
272+
expectedResult
273+
);
274+
275+
const restrictedPrecisionResult = {
276+
date_col: '2023-03-06',
277+
datetime_col: '2023-03-06T15:47:00.123000Z',
278+
datetime2_col: '2023-03-06T15:47:00.123456Z',
279+
smalldatetime_col: '2023-03-06T15:47:00.000000Z',
280+
time_col: '15:47:00.123456'
281+
};
282+
const restrictedPrecision = new CompatibilityContext({
283+
edition: 2,
284+
maxTimeValuePrecision: TimeValuePrecision.microseconds
285+
});
286+
expect(applyRowContext(databaseRows[0], restrictedPrecision)).toMatchObject(restrictedPrecisionResult);
287+
expect(applyRowContext(replicatedRows[0], restrictedPrecision)).toMatchObject(restrictedPrecisionResult);
257288
});
258289

259290
test('Date types edge cases mappings', async () => {
@@ -277,18 +308,22 @@ describe('MSSQL Data Types Tests', () => {
277308
await waitForPendingCDCChanges(beforeLSN, connectionManager);
278309

279310
const expectedResults = [
280-
{ datetime2_col: '0001-01-01T00:00:00.000Z' },
281-
{ datetime2_col: '9999-12-31T23:59:59.999Z' },
282-
{ datetime_col: '1753-01-01T00:00:00.000Z' },
283-
{ datetime_col: '9999-12-31T23:59:59.997Z' }
311+
{ datetime2_col: '0001-01-01T00:00:00.000000000Z' },
312+
{ datetime2_col: '9999-12-31T23:59:59.999000000Z' },
313+
{ datetime_col: '1753-01-01T00:00:00.000000000Z' },
314+
{ datetime_col: '9999-12-31T23:59:59.997000000Z' }
284315
];
285316

286317
const databaseRows = await getDatabaseRows(connectionManager, 'test_data');
287318
const replicatedRows = await getReplicatedRows(connectionManager, 'test_data');
288319

289320
for (let i = 0; i < expectedResults.length; i++) {
290-
expect(databaseRows[i]).toMatchObject(expectedResults[i]);
291-
expect(replicatedRows[i]).toMatchObject(expectedResults[i]);
321+
expect(applyRowContext(databaseRows[i], CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY)).toMatchObject(
322+
expectedResults[i]
323+
);
324+
expect(applyRowContext(replicatedRows[i], CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY)).toMatchObject(
325+
expectedResults[i]
326+
);
292327
}
293328
});
294329

@@ -302,15 +337,19 @@ describe('MSSQL Data Types Tests', () => {
302337
await waitForPendingCDCChanges(beforeLSN, connectionManager);
303338

304339
const expectedResult = {
305-
datetimeoffset_col: '2023-03-06T10:47:00.000Z' // Converted to UTC
340+
datetimeoffset_col: '2023-03-06T10:47:00.000000000Z' // Converted to UTC
306341
};
307342

308343
const databaseRows = await getDatabaseRows(connectionManager, 'test_data');
309344
const replicatedRows = await getReplicatedRows(connectionManager, 'test_data');
310345

311346
// Note: The driver converts DateTimeOffset to Date, which incorporates the timezone offset which is then represented in UTC.
312-
expect(databaseRows[0]).toMatchObject(expectedResult);
313-
expect(replicatedRows[0]).toMatchObject(expectedResult);
347+
expect(applyRowContext(databaseRows[0], CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY)).toMatchObject(
348+
expectedResult
349+
);
350+
expect(applyRowContext(replicatedRows[0], CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY)).toMatchObject(
351+
expectedResult
352+
);
314353
});
315354

316355
test('UniqueIdentifier type mapping', async () => {

modules/module-mysql/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@powersync/service-sync-rules": "workspace:*",
3434
"@powersync/service-types": "workspace:*",
3535
"@powersync/service-jsonbig": "workspace:*",
36-
"@powersync/mysql-zongji": "^0.5.0",
36+
"@powersync/mysql-zongji": "^0.6.0",
3737
"async": "^3.2.4",
3838
"mysql2": "^3.11.0",
3939
"node-sql-parser": "^5.3.9",

0 commit comments

Comments
 (0)