Skip to content

Commit 6364c02

Browse files
committed
Merge remote-tracking branch 'origin/main' into reduce-memory-usage
2 parents aa4eb0a + 27efd52 commit 6364c02

File tree

11 files changed

+186
-52
lines changed

11 files changed

+186
-52
lines changed

.changeset/wet-gorillas-remember.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/service-sync-rules': minor
3+
---
4+
5+
Expand supported combinations of the IN operator

.env.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Connections for tests
2-
MONGO_TEST_UR="mongodb://localhost:27017/powersync_test"
2+
MONGO_TEST_URL="mongodb://localhost:27017/powersync_test"
33
PG_TEST_URL="postgres://postgres:postgres@localhost:5432/powersync_test"

DEVELOP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pnpm build
2020

2121
The PowerSync service requires Postgres and MongoDB server connections. These configuration details can be specified in a `powersync.yaml` (or JSON) configuration file.
2222

23-
See the [Self hosting demo](https://github.com/powersync-ja/self-host-demo) for demos of starting these services.
23+
See the [self-hosting demo](https://github.com/powersync-ja/self-host-demo) for demos of starting these services.
2424

2525
A quick method for running all required services with a handy backend and frontend is to run the following in a checked-out `self-host-demo` folder.
2626

@@ -65,7 +65,7 @@ Some tests for these packages require a connection to MongoDB and Postgres. Conn
6565
These can be set in a terminal/shell
6666

6767
```bash
68-
export MONGO_TEST_UR="mongodb://localhost:27017/powersync_test"
68+
export MONGO_TEST_URL="mongodb://localhost:27017/powersync_test"
6969
export PG_TEST_URL="postgres://postgres:postgres@localhost:5432/powersync_test"
7070
```
7171

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/7372448/d2538c43-c1a0-4c47-9a76-41462dba484f"/></a>
33
</p>
44

5-
_[PowerSync](https://www.powersync.com) is a Postgres-SQLite sync engine, which helps developers to create local-first real-time reactive apps that work seamlessly both online and offline._
5+
*[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres or MongoDB on the server-side (MySQL coming soon).*
66

77
# PowerSync Service
88

9-
`powersync-service` is the monorepo for the core [PowerSync service](https://docs.powersync.com/architecture/powersync-service).
9+
`powersync-service` is the monorepo for the core [PowerSync Service](https://docs.powersync.com/architecture/powersync-service).
1010

1111
The service can be started using the public Docker image. See the image [notes](./service/README.md)
1212

@@ -19,7 +19,7 @@ The service can be started using the public Docker image. See the image [notes](
1919

2020
- [packages/jpgwire](./packages/jpgwire/README.md)
2121

22-
- Customized version of [pgwire](https://www.npmjs.com/package/pgwire?activeTab=dependencies)
22+
- Customized version of [pgwire](https://www.npmjs.com/package/pgwire?activeTab=dependencies) (used with Postgres)
2323

2424
- [packages/jsonbig](./packages/jsonbig/README.md)
2525

@@ -33,8 +33,8 @@ The service can be started using the public Docker image. See the image [notes](
3333

3434
- Library containing logic for PowerSync sync rules
3535

36-
- [packages/types](./packages/types/README.md)
37-
- Type definitions for the PowerSync service
36+
- [packages/types](./packages/types/)
37+
- Type definitions for the PowerSync Service
3838

3939
## Libraries
4040

@@ -46,7 +46,7 @@ The service can be started using the public Docker image. See the image [notes](
4646

4747
- [service](./service/README.md)
4848

49-
Contains the PowerSync service code. This project is used to build the `journeyapps/powersync-service` Docker image.
49+
Contains the PowerSync Service code. This project is used to build the `journeyapps/powersync-service` Docker image.
5050

5151
## Docs
5252

packages/sync-rules/src/StaticSqlParameterQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export class StaticSqlParameterQuery {
117117

118118
get usesUnauthenticatedRequestParameters(): boolean {
119119
// select where request.parameters() ->> 'include_comments'
120-
const unauthenticatedFilter = this.filter!.usesUnauthenticatedRequestParameters;
120+
const unauthenticatedFilter = this.filter?.usesUnauthenticatedRequestParameters;
121121

122122
// select request.parameters() ->> 'project_id'
123123
const unauthenticatedExtractor =

packages/sync-rules/src/sql_filters.ts

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ExpressionType, TYPE_NONE } from './ExpressionType.js';
44
import { SqlRuleError } from './errors.js';
55
import {
66
BASIC_OPERATORS,
7+
OPERATOR_IN,
78
OPERATOR_IS_NOT_NULL,
89
OPERATOR_IS_NULL,
910
OPERATOR_JSON_EXTRACT_JSON,
@@ -302,13 +303,18 @@ export class SqlTools {
302303
throw new Error('Unexpected');
303304
}
304305
} else if (op == 'IN') {
305-
// Options:
306-
// static IN static
307-
// parameterValue IN static
308-
309-
if (isRowValueClause(leftFilter) && isRowValueClause(rightFilter)) {
310-
// static1 IN static2
311-
return compileStaticOperator(op, leftFilter, rightFilter);
306+
// Special cases:
307+
// parameterValue IN rowValue
308+
// rowValue IN parameterValue
309+
// All others are handled by standard function composition
310+
311+
const composeType = this.getComposeType(OPERATOR_IN, [leftFilter, rightFilter], [left, right]);
312+
if (composeType.errorClause != null) {
313+
return composeType.errorClause;
314+
} else if (composeType.argsType != null) {
315+
// This is a standard supported configuration, takes precedence over
316+
// the special cases below.
317+
return this.composeFunction(OPERATOR_IN, [leftFilter, rightFilter], [left, right]);
312318
} else if (isParameterValueClause(leftFilter) && isRowValueClause(rightFilter)) {
313319
// token_parameters.value IN table.some_array
314320
// bucket.param IN table.some_array
@@ -371,7 +377,8 @@ export class SqlTools {
371377
usesUnauthenticatedRequestParameters: rightFilter.usesUnauthenticatedRequestParameters
372378
} satisfies ParameterMatchClause;
373379
} else {
374-
return this.error(`Unsupported usage of IN operator`, expr);
380+
// Not supported, return the error previously computed
381+
return this.error(composeType.error!, composeType.errorExpr);
375382
}
376383
} else if (BASIC_OPERATORS.has(op)) {
377384
const fnImpl = getOperatorFunction(op);
@@ -634,36 +641,19 @@ export class SqlTools {
634641
* @returns a compiled function clause
635642
*/
636643
composeFunction(fnImpl: SqlFunction, argClauses: CompiledClause[], debugArgExpressions: Expr[]): CompiledClause {
637-
let argsType: 'static' | 'row' | 'param' = 'static';
638-
for (let i = 0; i < argClauses.length; i++) {
639-
const debugArg = debugArgExpressions[i];
640-
const clause = argClauses[i];
641-
if (isClauseError(clause)) {
642-
// Return immediately on error
643-
return clause;
644-
} else if (isStaticValueClause(clause)) {
645-
// argsType unchanged
646-
} else if (isParameterValueClause(clause)) {
647-
if (!this.supports_parameter_expressions) {
648-
return this.error(`Cannot use bucket parameters in expressions`, debugArg);
649-
}
650-
if (argsType == 'static' || argsType == 'param') {
651-
argsType = 'param';
652-
} else {
653-
return this.error(`Cannot use table values and parameters in the same clauses`, debugArg);
654-
}
655-
} else if (isRowValueClause(clause)) {
656-
if (argsType == 'static' || argsType == 'row') {
657-
argsType = 'row';
658-
} else {
659-
return this.error(`Cannot use table values and parameters in the same clauses`, debugArg);
660-
}
661-
} else {
662-
return this.error(`Parameter match clauses cannot be used here`, debugArg);
663-
}
644+
const result = this.getComposeType(fnImpl, argClauses, debugArgExpressions);
645+
if (result.errorClause != null) {
646+
return result.errorClause;
647+
} else if (result.error != null) {
648+
return this.error(result.error, result.errorExpr);
664649
}
650+
const argsType = result.argsType!;
665651

666-
if (argsType == 'row' || argsType == 'static') {
652+
if (argsType == 'static') {
653+
const args = argClauses.map((e) => (e as StaticValueClause).value);
654+
const evaluated = fnImpl.call(...args);
655+
return staticValueClause(evaluated);
656+
} else if (argsType == 'row') {
667657
return {
668658
evaluate: (tables) => {
669659
const args = argClauses.map((e) => (e as RowValueClause).evaluate(tables));
@@ -705,7 +695,48 @@ export class SqlTools {
705695
}
706696
}
707697

708-
parameterFunction() {}
698+
getComposeType(
699+
fnImpl: SqlFunction,
700+
argClauses: CompiledClause[],
701+
debugArgExpressions: Expr[]
702+
): { argsType?: string; error?: string; errorExpr?: Expr; errorClause?: ClauseError } {
703+
let argsType: 'static' | 'row' | 'param' = 'static';
704+
for (let i = 0; i < argClauses.length; i++) {
705+
const debugArg = debugArgExpressions[i];
706+
const clause = argClauses[i];
707+
if (isClauseError(clause)) {
708+
// Return immediately on error
709+
return { errorClause: clause };
710+
} else if (isStaticValueClause(clause)) {
711+
// argsType unchanged
712+
} else if (isParameterValueClause(clause)) {
713+
if (!this.supports_parameter_expressions) {
714+
if (fnImpl.debugName == 'operatorIN') {
715+
// Special-case error message to be more descriptive
716+
return { error: `Cannot use bucket parameters on the right side of IN operators`, errorExpr: debugArg };
717+
}
718+
return { error: `Cannot use bucket parameters in expressions`, errorExpr: debugArg };
719+
}
720+
if (argsType == 'static' || argsType == 'param') {
721+
argsType = 'param';
722+
} else {
723+
return { error: `Cannot use table values and parameters in the same clauses`, errorExpr: debugArg };
724+
}
725+
} else if (isRowValueClause(clause)) {
726+
if (argsType == 'static' || argsType == 'row') {
727+
argsType = 'row';
728+
} else {
729+
return { error: `Cannot use table values and parameters in the same clauses`, errorExpr: debugArg };
730+
}
731+
} else {
732+
return { error: `Parameter match clauses cannot be used here`, errorExpr: debugArg };
733+
}
734+
}
735+
736+
return {
737+
argsType
738+
};
739+
}
709740
}
710741

711742
function isStatic(expr: Expr) {

packages/sync-rules/src/sql_functions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { JSONBig } from '@powersync/service-jsonbig';
2-
import { SQLITE_FALSE, SQLITE_TRUE, sqliteBool, sqliteNot } from './sql_support.js';
2+
import { getOperatorFunction, SQLITE_FALSE, SQLITE_TRUE, sqliteBool, sqliteNot } from './sql_support.js';
33
import { SqliteValue } from './types.js';
44
import { jsonValueToSqlite } from './utils.js';
55
// Declares @syncpoint/wkx module
@@ -787,6 +787,8 @@ export const OPERATOR_NOT: SqlFunction = {
787787
}
788788
};
789789

790+
export const OPERATOR_IN = getOperatorFunction('IN');
791+
790792
export function castOperator(castTo: string | undefined): SqlFunction | null {
791793
if (castTo == null || !CAST_TYPES.has(castTo)) {
792794
return null;

packages/sync-rules/test/src/data_queries.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,38 @@ describe('data queries', () => {
4141
expect(query.evaluateRow(ASSETS, { id: 'asset1', org_id: null })).toEqual([]);
4242
});
4343

44+
test('static IN data query', function () {
45+
const sql = `SELECT * FROM assets WHERE 'green' IN assets.categories`;
46+
const query = SqlDataQuery.fromSql('mybucket', [], sql);
47+
expect(query.errors).toEqual([]);
48+
49+
expect(query.evaluateRow(ASSETS, { id: 'asset1', categories: JSON.stringify(['red', 'green']) })).toMatchObject([
50+
{
51+
bucket: 'mybucket[]',
52+
table: 'assets',
53+
id: 'asset1'
54+
}
55+
]);
56+
57+
expect(query.evaluateRow(ASSETS, { id: 'asset1', categories: JSON.stringify(['red', 'blue']) })).toEqual([]);
58+
});
59+
60+
test('data IN static query', function () {
61+
const sql = `SELECT * FROM assets WHERE assets.condition IN '["good","great"]'`;
62+
const query = SqlDataQuery.fromSql('mybucket', [], sql);
63+
expect(query.errors).toEqual([]);
64+
65+
expect(query.evaluateRow(ASSETS, { id: 'asset1', condition: 'good' })).toMatchObject([
66+
{
67+
bucket: 'mybucket[]',
68+
table: 'assets',
69+
id: 'asset1'
70+
}
71+
]);
72+
73+
expect(query.evaluateRow(ASSETS, { id: 'asset1', condition: 'bad' })).toEqual([]);
74+
});
75+
4476
test('table alias', function () {
4577
const sql = 'SELECT * FROM assets as others WHERE others.org_id = bucket.org_id';
4678
const query = SqlDataQuery.fromSql('mybucket', ['org_id'], sql);
@@ -158,7 +190,9 @@ describe('data queries', () => {
158190
test('invalid query - invalid IN', function () {
159191
const sql = 'SELECT * FROM assets WHERE assets.category IN bucket.categories';
160192
const query = SqlDataQuery.fromSql('mybucket', ['categories'], sql);
161-
expect(query.errors).toMatchObject([{ type: 'fatal', message: 'Unsupported usage of IN operator' }]);
193+
expect(query.errors).toMatchObject([
194+
{ type: 'fatal', message: 'Cannot use bucket parameters on the right side of IN operators' }
195+
]);
162196
});
163197

164198
test('invalid query - not all parameters used', function () {

packages/sync-rules/test/src/static_parameter_queries.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from 'vitest';
2-
import { SqlParameterQuery } from '../../src/index.js';
2+
import { RequestParameters, SqlParameterQuery } from '../../src/index.js';
33
import { StaticSqlParameterQuery } from '../../src/StaticSqlParameterQuery.js';
44
import { normalizeTokenParameters } from './util.js';
55

@@ -82,6 +82,68 @@ describe('static parameter queries', () => {
8282
expect(query.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["user1"]']);
8383
});
8484

85+
test('static value', function () {
86+
const sql = `SELECT WHERE 1`;
87+
const query = SqlParameterQuery.fromSql('mybucket', sql) as StaticSqlParameterQuery;
88+
expect(query.errors).toEqual([]);
89+
expect(query.getStaticBucketIds(new RequestParameters({ sub: '' }, {}))).toEqual(['mybucket[]']);
90+
});
91+
92+
test('static expression (1)', function () {
93+
const sql = `SELECT WHERE 1 = 1`;
94+
const query = SqlParameterQuery.fromSql('mybucket', sql) as StaticSqlParameterQuery;
95+
expect(query.errors).toEqual([]);
96+
expect(query.getStaticBucketIds(new RequestParameters({ sub: '' }, {}))).toEqual(['mybucket[]']);
97+
});
98+
99+
test('static expression (2)', function () {
100+
const sql = `SELECT WHERE 1 != 1`;
101+
const query = SqlParameterQuery.fromSql('mybucket', sql) as StaticSqlParameterQuery;
102+
expect(query.errors).toEqual([]);
103+
expect(query.getStaticBucketIds(new RequestParameters({ sub: '' }, {}))).toEqual([]);
104+
});
105+
106+
test('static IN expression', function () {
107+
const sql = `SELECT WHERE 'admin' IN '["admin", "superuser"]'`;
108+
const query = SqlParameterQuery.fromSql('mybucket', sql, undefined, {}) as StaticSqlParameterQuery;
109+
expect(query.errors).toEqual([]);
110+
expect(query.getStaticBucketIds(new RequestParameters({ sub: '' }, {}))).toEqual(['mybucket[]']);
111+
});
112+
113+
test('IN for permissions in request.jwt() (1)', function () {
114+
// Can use -> or ->> here
115+
const sql = `SELECT 'read:users' IN (request.jwt() ->> 'permissions') as access_granted`;
116+
const query = SqlParameterQuery.fromSql('mybucket', sql) as StaticSqlParameterQuery;
117+
expect(query.errors).toEqual([]);
118+
expect(
119+
query.getStaticBucketIds(new RequestParameters({ sub: '', permissions: ['write', 'read:users'] }, {}))
120+
).toEqual(['mybucket[1]']);
121+
expect(
122+
query.getStaticBucketIds(new RequestParameters({ sub: '', permissions: ['write', 'write:users'] }, {}))
123+
).toEqual(['mybucket[0]']);
124+
});
125+
126+
test('IN for permissions in request.jwt() (2)', function () {
127+
// Can use -> or ->> here
128+
const sql = `SELECT WHERE 'read:users' IN (request.jwt() ->> 'permissions')`;
129+
const query = SqlParameterQuery.fromSql('mybucket', sql, undefined, {}) as StaticSqlParameterQuery;
130+
expect(query.errors).toEqual([]);
131+
expect(
132+
query.getStaticBucketIds(new RequestParameters({ sub: '', permissions: ['write', 'read:users'] }, {}))
133+
).toEqual(['mybucket[]']);
134+
expect(
135+
query.getStaticBucketIds(new RequestParameters({ sub: '', permissions: ['write', 'write:users'] }, {}))
136+
).toEqual([]);
137+
});
138+
139+
test('IN for permissions in request.jwt() (3)', function () {
140+
const sql = `SELECT WHERE request.jwt() ->> 'role' IN '["admin", "superuser"]'`;
141+
const query = SqlParameterQuery.fromSql('mybucket', sql, undefined, {}) as StaticSqlParameterQuery;
142+
expect(query.errors).toEqual([]);
143+
expect(query.getStaticBucketIds(new RequestParameters({ sub: '', role: 'superuser' }, {}))).toEqual(['mybucket[]']);
144+
expect(query.getStaticBucketIds(new RequestParameters({ sub: '', role: 'superadmin' }, {}))).toEqual([]);
145+
});
146+
85147
test('case-sensitive queries (1)', () => {
86148
const sql = 'SELECT request.user_id() as USER_ID';
87149
const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;

service/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/7372448/d2538c43-c1a0-4c47-9a76-41462dba484f"/></a>
33
</p>
44

5-
*[PowerSync](https://www.powersync.com) is a Postgres-SQLite sync engine, which helps developers to create local-first real-time reactive apps that work seamlessly both online and offline.*
5+
*[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres or MongoDB on the server-side (MySQL coming soon).*
66

77
# Quick reference
88

0 commit comments

Comments
 (0)