Skip to content
Draft
84 changes: 84 additions & 0 deletions packages/sync-rules/grammar/bucket-definitions.ebnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
PowerSync sync-rules SQL grammar: bucket_definitions mode

SQL appears in YAML at:
- bucket_definitions.<bucket_name>.parameters
- string or array of strings
- bucket_definitions.<bucket_name>.data[]
*/

/* Parameter-query SQL forms used under bucket_definitions.<name>.parameters */
ParameterQuery ::= TableValuedParameterQuery | TableParameterQuery | StaticParameterQuery

StaticParameterQuery ::= "SELECT" SelectList ("WHERE" MatchExpr)?

TableParameterQuery ::= "SELECT" SelectList "FROM" TableRef ("WHERE" MatchExpr)?

/* Only json_each(...) table-valued FROM is supported in this mode. */
TableValuedParameterQuery ::= "SELECT" SelectList "FROM" JsonEachCall Alias? ("WHERE" MatchExpr)?

JsonEachCall ::= "JSON_EACH" "(" ScalarExpr ")"

/* Data-query SQL forms used under bucket_definitions.<name>.data[] */
DataQuery ::= "SELECT" DataSelectList "FROM" TableRef ("WHERE" DataMatchExpr)?

SelectList ::= SelectItem ("," SelectItem)*

DataSelectList ::= DataSelectItem ("," DataSelectItem)*

SelectItem ::= ScalarExpr Alias?

DataSelectItem ::= "*" | ScalarExpr Alias?

Alias ::= "AS" Identifier

TableRef ::= Identifier ("." Identifier)?

DataMatchExpr ::= MatchExpr

MatchExpr ::= OrExpr

/* WHERE boolean expression subset */
OrExpr ::= AndExpr ("OR" AndExpr)*

AndExpr ::= UnaryExpr ("AND" UnaryExpr)*

UnaryExpr ::= "NOT"? MatchAtom

MatchAtom ::= Predicate | "(" MatchExpr ")"

Predicate ::= ScalarExpr PredicateTail | ScalarExpr

PredicateTail ::= "=" ScalarExpr | "IN" ScalarExpr | "&&" ScalarExpr | "IS" "NOT"? "NULL"

/* Scalar expression subset (sql_filters/sql_functions) */
ScalarExpr ::= ValueTerm (BinaryOp ValueTerm)*

ValueTerm ::= PrimaryTerm MemberSuffix*

PrimaryTerm ::= Literal | CastExpr | FunctionCall | Reference | "(" ScalarExpr ")"

MemberSuffix ::= ("->>" | "->") (StringLiteral | IntegerLiteral)

CastExpr ::= "CAST" "(" ScalarExpr "AS" CastType ")"

FunctionCall ::= (Identifier ".")? Identifier "(" ArgList? ")"

ArgList ::= ScalarExpr ("," ScalarExpr)*

Reference ::= Identifier ("." Identifier)?

BinaryOp ::= "=" | "!=" | "<" | ">" | "<=" | ">=" | "+" | "-" | "*" | "/" | "%" | "||" | "|" | "&" | "<<" | ">>"

CastType ::= "TEXT" | "INTEGER" | "REAL" | "NUMERIC" | "BLOB"

/* Identifiers are matched after SQL normalization to uppercase in tests. */
Identifier ::= [A-Z_][A-Z_0-9]*

Literal ::= StringLiteral | NumericLiteral | "TRUE" | "FALSE" | "NULL"

StringLiteral ::= "'" ([#x20-#x26] | [#x28-#x7E])* "'"

IntegerLiteral ::= [0-9]+

NumericLiteral ::= [0-9]+ ("." [0-9]+)?
84 changes: 84 additions & 0 deletions packages/sync-rules/grammar/sync-streams-alpha.ebnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
PowerSync sync-rules SQL grammar: sync streams alpha (syncStreamFromSql path, not new compiler).

SQL appears in YAML at:
- streams.<stream_name>.query

Notes:
- This is the sync streams alpha SQL parser path.
- YAML keys `with` and `queries` are not supported in this mode.
*/

/* SQL form used under streams.<name>.query in sync streams alpha mode */
SyncStreamsAlphaQuery ::= "SELECT" StreamSelectList "FROM" TableRef ("WHERE" StreamWhereExpr)?

StreamSelectList ::= StreamSelectItem ("," StreamSelectItem)*

StreamSelectItem ::= "*" | ScalarExpr Alias?

StreamWhereExpr ::= OrExpr

/* WHERE boolean expression subset */
OrExpr ::= AndExpr ("OR" AndExpr)*

AndExpr ::= UnaryExpr ("AND" UnaryExpr)*

UnaryExpr ::= "NOT"? StreamMatchAtom

StreamMatchAtom ::= StreamPredicate | "(" StreamWhereExpr ")"

StreamPredicate ::= ScalarExpr StreamPredicateTail | ScalarExpr

/* IN/NOT IN/&& may target a scalar expression or a restricted subquery */
StreamPredicateTail ::= "=" ScalarExpr | "IN" InSource | "NOT" "IN" InSource | "&&" InSource | "IS" "NOT"? "NULL"

InSource ::= ScalarExpr | StreamSubquery

StreamSubquery ::= "(" "SELECT" SubqueryResultExpr "FROM" TableRef ("WHERE" SubqueryWhereExpr)? ")"

SubqueryResultExpr ::= ScalarExpr

/* Sync streams alpha subquery filters are AND-only (no OR) */
SubqueryWhereExpr ::= SubqueryAndExpr

SubqueryAndExpr ::= SubqueryPredicate ("AND" SubqueryPredicate)*

SubqueryPredicate ::= ScalarExpr SubqueryPredicateTail | ScalarExpr

SubqueryPredicateTail ::= "=" ScalarExpr | "IN" ScalarExpr | "&&" ScalarExpr | "IS" "NOT"? "NULL"

/* Scalar expression subset (sql_filters/sql_functions) */
ScalarExpr ::= ValueTerm (BinaryOp ValueTerm)*

ValueTerm ::= PrimaryTerm MemberSuffix*

PrimaryTerm ::= Literal | CastExpr | FunctionCall | Reference | "(" ScalarExpr ")"

MemberSuffix ::= ("->>" | "->") (StringLiteral | IntegerLiteral)

CastExpr ::= "CAST" "(" ScalarExpr "AS" CastType ")"

FunctionCall ::= (Identifier ".")? Identifier "(" ArgList? ")"

ArgList ::= ScalarExpr ("," ScalarExpr)*

Reference ::= Identifier ("." Identifier)?

Alias ::= "AS" Identifier

TableRef ::= Identifier ("." Identifier)?

BinaryOp ::= "=" | "!=" | "<" | ">" | "<=" | ">=" | "+" | "-" | "*" | "/" | "%" | "||" | "|" | "&" | "<<" | ">>"

CastType ::= "TEXT" | "INTEGER" | "REAL" | "NUMERIC" | "BLOB"

/* Identifiers are matched after SQL normalization to uppercase in tests. */
Identifier ::= [A-Z_][A-Z_0-9]*

Literal ::= StringLiteral | NumericLiteral | "TRUE" | "FALSE" | "NULL"

StringLiteral ::= "'" ([#x20-#x26] | [#x28-#x7E])* "'"

IntegerLiteral ::= [0-9]+

NumericLiteral ::= [0-9]+ ("." [0-9]+)?
112 changes: 112 additions & 0 deletions packages/sync-rules/grammar/sync-streams-compiler.ebnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
PowerSync sync-rules SQL grammar: new sync streams compiler.

SQL appears in YAML at:
- streams.<stream_name>.query
- streams.<stream_name>.queries[]
- streams.<stream_name>.with.<cte_name>

Notes:
- This grammar is used when config.sync_config_compiler = true (and edition >= 2)
*/

/* Top-level stream query form: streams.<name>.query / streams.<name>.queries[] */
CompilerStreamQuery ::= "SELECT" ResultColumnList "FROM" FromSource FromContinuation* ("WHERE" WhereExpr)?

ResultColumnList ::= ResultColumn ("," ResultColumn)*

/* Supports wildcard, table wildcard, or scalar expression columns */
ResultColumn ::= "*" | Reference "." "*" | ScalarExpr Alias?

FromContinuation ::= "," FromSource | JoinClause

FromSource ::= TableValuedSource | SubquerySource | TableSource

/* Table and table-valued sources may be unaliased or AS-aliased */
TableSource ::= TableRef Alias | TableRef

TableValuedSource ::= TableValuedCall Alias | TableValuedCall

/* Subqueries require an alias; optional column-name list is supported */
SubquerySource ::= "(" CompilerSubquery ")" Alias ("(" ColumnNameList ")")?

JoinClause ::= "INNER"? "JOIN" FromSource ("ON" WhereExpr)?

TableValuedCall ::= Identifier "(" ArgList? ")"

WhereExpr ::= OrExpr

/* WHERE boolean expression subset */
OrExpr ::= AndExpr ("OR" AndExpr)*

AndExpr ::= UnaryExpr ("AND" UnaryExpr)*

UnaryExpr ::= "NOT"? WhereAtom

WhereAtom ::= Predicate | "(" WhereExpr ")"

Predicate ::= ScalarExpr PredicateTail | ScalarExpr

/* Includes IN/NOT IN, overlap, null checks, and BETWEEN */
PredicateTail ::= "=" ScalarExpr | "IN" InSource | "NOT" "IN" InSource | "&&" InSource | "IS" "NOT"? "NULL" | "NOT"? "BETWEEN" ScalarExpr "AND" ScalarExpr

InSource ::= "(" CompilerSubquery ")" | CteShorthandRef | ScalarExpr

/* CTE shorthand supports expressions like: x IN cte_name */
CteShorthandRef ::= Identifier

CompilerSubquery ::= "SELECT" ResultColumnList "FROM" FromSource FromContinuation* ("WHERE" WhereExpr)?

/* CTE bodies (streams.<name>.with.<cte_name>) disallow wildcard columns by using ScalarExpr columns only */
CompilerCteSubquery ::= "SELECT" CteResultColumnList "FROM" FromSource FromContinuation* ("WHERE" WhereExpr)?

CteResultColumnList ::= CteResultColumn ("," CteResultColumn)*

CteResultColumn ::= ScalarExpr Alias?

/* Scalar expression subset lowered by compiler/sqlite.ts */
ScalarExpr ::= ValueTerm (BinaryOp ValueTerm)*

ValueTerm ::= PrimaryTerm MemberSuffix*

PrimaryTerm ::= Literal | CaseExpr | CastExpr | FunctionCall | Reference | "(" ScalarExpr ")"

MemberSuffix ::= ("->>" | "->") (StringLiteral | IntegerLiteral)

CastExpr ::= "CAST" "(" ScalarExpr "AS" CastType ")"

/* CASE expression subset supported by compiler parser */
CaseExpr ::= SearchedCaseExpr | SimpleCaseExpr

SearchedCaseExpr ::= "CASE" "WHEN" CaseCondition "THEN" ScalarExpr ("WHEN" CaseCondition "THEN" ScalarExpr)* ("ELSE" ScalarExpr)? "END"

SimpleCaseExpr ::= "CASE" ScalarExpr "WHEN" ScalarExpr "THEN" ScalarExpr ("WHEN" ScalarExpr "THEN" ScalarExpr)* ("ELSE" ScalarExpr)? "END"

CaseCondition ::= OrExpr

FunctionCall ::= (Identifier ".")? Identifier "(" ArgList? ")"

ArgList ::= ScalarExpr ("," ScalarExpr)*

Reference ::= Identifier ("." Identifier)?

Alias ::= "AS" Identifier

TableRef ::= Identifier ("." Identifier)?

ColumnNameList ::= Identifier ("," Identifier)*

BinaryOp ::= "=" | "!=" | "<" | ">" | "<=" | ">=" | "+" | "-" | "*" | "/" | "%" | "||" | "|" | "&" | "<<" | ">>"

CastType ::= "TEXT" | "INTEGER" | "REAL" | "NUMERIC" | "BLOB"

/* Identifiers are matched after SQL normalization to uppercase in tests. */
Identifier ::= [A-Z_][A-Z_0-9]*

Literal ::= StringLiteral | NumericLiteral | "TRUE" | "FALSE" | "NULL"

StringLiteral ::= "'" ([#x20-#x26] | [#x28-#x7E])* "'"

IntegerLiteral ::= [0-9]+

NumericLiteral ::= [0-9]+ ("." [0-9]+)?
1 change: 1 addition & 0 deletions packages/sync-rules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"devDependencies": {
"@types/node": "^22.16.2",
"ebnf": "^1.9.1",
"vitest": "catalog:"
}
}
1 change: 1 addition & 0 deletions packages/sync-rules/src/compiler/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ export class PostgresToSqlite {
low: this.translateNodeWithLocation(expr.lo),
high: this.translateNodeWithLocation(expr.hi)
};
this.options.locations.sourceForNode.set(between, expr);

return expr.op === 'NOT BETWEEN' ? this.negate(expr, between) : between;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, test } from 'vitest';
import {
assertGrammarExpectation,
assertParserExpectation,
loadFixtureFile,
runGrammarChecker,
runParser
} from './parity_helpers.js';

const fixtures = loadFixtureFile('fixtures/bucket_definitions.yaml', 'bucket_definitions');

describe('grammar parity fixtures: bucket_definitions', () => {
test.each(fixtures)('parser contract: $slot/$kind/$label', (fixture) => {
const outcome = runParser(fixture);
assertParserExpectation(fixture, outcome);
});

test.each(fixtures)('grammar contract: $slot/$kind/$label', (fixture) => {
const outcome = runGrammarChecker(fixture);
assertGrammarExpectation(fixture, outcome);
});
});
Loading