Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 122 additions & 19 deletions crates/squawk_parser/src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3580,7 +3580,7 @@ fn opt_sort_order(p: &mut Parser<'_>) {
}
USING_KW => {
p.bump(USING_KW);
operator(p);
op_or_opcall(p);
SORT_USING
}
_ => {
Expand Down Expand Up @@ -5048,19 +5048,122 @@ fn opt_operator(p: &mut Parser<'_>) -> bool {
p.eat(kind)
}

fn opt_op(p: &mut Parser<'_>) -> bool {
if !p.at_ts(OPERATOR_FIRST) || p.at(FAT_ARROW) {
return false;
}
let (power, kind, _) = current_op(p, &Restrictions::default());
if power == 0 {
p.bump_any();
return true;
}
p.eat(kind)
}

const OP_RECOVERY: TokenSet = TokenSet::new(&[L_PAREN, COMMA]).union(EXPR_RECOVERY_SET);

fn err_recover_run(p: &mut Parser<'_>, message: String, recovery: TokenSet) {
if p.at(EOF) || p.at_ts(recovery) {
p.error(message);
return;
}
let m = p.start();
p.error(message);
while !p.at(EOF) && !p.at_ts(recovery) {
p.bump_any();
}
m.complete(p, ERROR);
}

fn op(p: &mut Parser<'_>) {
if opt_op(p) {
return;
}
let message = format!("expected operator, got {:?}", p.current());
if p.at(EOF) || p.at_ts(OP_RECOVERY) {
p.error(message);
return;
}
let m = p.start();
p.error(message);
if !p.eat(COLON_COLON) && !p.eat(COLON_EQ) && !p.eat(FAT_ARROW) {
p.bump_any();
}
m.complete(p, ERROR);
}

fn opt_op_or_opcall(p: &mut Parser<'_>) -> bool {
p.eat(OPERATOR_CALL) || opt_op(p)
}

fn op_or_opcall(p: &mut Parser<'_>) {
if p.eat(OPERATOR_CALL) {
return;
}
let m = p.start();
if p.nth_at(1, DOT) {
opt_op_path(p);
p.error("qualified operator requires OPERATOR(...)");
}
op(p);
m.complete(p, OP);
}

fn at_op_path_segment(p: &Parser<'_>) -> bool {
p.at_ts(NAME_FIRST) || (!p.at(DOT) && p.nth_at(1, DOT))
}

fn opt_op_path(p: &mut Parser<'_>) -> Option<CompletedMarker> {
if !at_op_path_segment(p) {
return None;
}
let m = p.start();
op_path_segment(p);
let mut qual = m.complete(p, PATH_REF);
while p.at(DOT) {
let path = qual.precede(p);
p.bump(DOT);
let named = at_op_path_segment(p);
op_path_segment(p);
qual = path.complete(p, PATH_REF);
if !named {
break;
}
}
Some(qual)
}

fn op_path_segment(p: &mut Parser<'_>) {
let m = p.start();
if p.at_ts(NAME_FIRST) {
pg_name(p);
} else if at_op_path_segment(p) {
p.err_and_bump("expected name");
}
m.complete(p, PATH_SEGMENT_REF);
}

// optional schema supported
// >
// bar.>
// foo.bar.>
pub(crate) fn operator(p: &mut Parser<'_>) {
pub(crate) fn qual_op(p: &mut Parser<'_>) {
let m = p.start();
opt_path_name_ref(p);
if !opt_operator(p) {
p.error(format!("expected operator, got {:?}", p.current()));
}
opt_op_path(p);
op(p);
m.complete(p, OP);
}

// >
// bar.>
// foo.bar.>
// operator(bar.>)
fn qual_op_or_opcall(p: &mut Parser<'_>) {
if !p.eat(OPERATOR_CALL) {
qual_op(p);
}
}

pub(crate) fn current_operator(p: &Parser<'_>) -> Option<SyntaxKind> {
let (power, kind, _) = current_op(p, &Restrictions::default());
if power == 0 { None } else { Some(kind) }
Expand Down Expand Up @@ -5274,10 +5377,7 @@ fn opt_constraint_exclusion(p: &mut Parser<'_>) -> Option<CompletedMarker> {
return None;
}
p.expect(WITH_KW);
// support:
// with >
// with foo.bar.buzz.>
operator(p);
qual_op_or_opcall(p);
Some(m.complete(p, CONSTRAINT_EXCLUSION))
}

Expand Down Expand Up @@ -9152,7 +9252,7 @@ fn extension_member_object(p: &mut Parser<'_>) {
}
OPERATOR_KW => {
p.bump(OPERATOR_KW);
operator(p);
qual_op(p);
p.expect(L_PAREN);
type_name(p);
p.expect(COMMA);
Expand Down Expand Up @@ -10804,7 +10904,7 @@ fn comment_object(p: &mut Parser<'_>) {
}
OPERATOR_KW => {
p.bump(OPERATOR_KW);
operator(p);
qual_op(p);
p.eat(L_PAREN);
type_name(p);
p.expect(COMMA);
Expand Down Expand Up @@ -12182,7 +12282,7 @@ fn create_operator(p: &mut Parser<'_>) -> CompletedMarker {
let m = p.start();
p.bump(CREATE_KW);
p.bump(OPERATOR_KW);
operator(p);
qual_op(p);
attribute_list(p);
p.eat(SEMICOLON);
m.complete(p, CREATE_OPERATOR)
Expand Down Expand Up @@ -12234,7 +12334,7 @@ fn operator_class_option(p: &mut Parser<'_>) {
if opt_numeric_literal(p).is_none() {
p.error("expected an integer");
}
operator(p);
qual_op(p);
if p.eat(L_PAREN) {
type_name(p);
p.expect(COMMA);
Expand Down Expand Up @@ -13418,7 +13518,7 @@ fn op_sig_list(p: &mut Parser<'_>) {
// name ( { left_type | NONE } , right_type )
fn operator_sig(p: &mut Parser<'_>) {
let m = p.start();
operator(p);
qual_op(p);
p.expect(L_PAREN);
if !p.eat(NONE_KW) {
type_name(p);
Expand Down Expand Up @@ -18675,22 +18775,25 @@ fn opt_attribute_option(p: &mut Parser<'_>) -> bool {
// qual_all_Op:
// | all_Op
// | OPERATOR '(' any_operator ')'
const ATTRIBUTE_VALUE_RECOVERY: TokenSet = TokenSet::new(&[COMMA]).union(EXPR_RECOVERY_SET);

fn def_arg(p: &mut Parser<'_>) {
let m = p.start();
if opt_bool_literal(p)
|| opt_string_literal(p).is_some()
|| opt_numeric_literal(p).is_some()
|| opt_operator(p)
|| opt_op_or_opcall(p)
|| p.eat(NONE_KW)
{
} else if p.at_ts(RESERVED_KEYWORDS) {
p.bump_any();
} else if p.eat(OPERATOR_KW) {
p.expect(L_PAREN);
operator(p);
qual_op(p);
p.expect(R_PAREN);
} else {
opt_type_name(p);
} else if !opt_type_name(p) {
let message = format!("expected attribute value, got {:?}", p.current());
err_recover_run(p, message, ATTRIBUTE_VALUE_RECOVERY);
}
m.complete(p, ATTRIBUTE_VALUE);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/squawk_parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ impl<'t> Parser<'t> {
self.bump(SyntaxKind::L_PAREN);

// e.g. `+`, `pg_catalog.+`, `db.pg_catalog.+`
grammar::operator(self);
grammar::qual_op(self);

self.expect(SyntaxKind::R_PAREN);
m.complete(self, SyntaxKind::OPERATOR_CALL);
Expand Down
29 changes: 29 additions & 0 deletions crates/squawk_parser/tests/data/err/operator.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- all_Op is symbolic only, keyword operators aren't operator names
select 1 operator(and) 2;
select 1 operator(or) 2;
select 1 operator(in) 2;
create operator and (leftarg = int, rightarg = int, function = int4pl);
drop operator and (int, int);

-- neither are the tokens Postgres reserves
select 1 operator(::) 2;
select 1 operator(:=) 2;
select 1 operator(=>) 2;
create operator => (rightarg = int8, function = factorial);
create operator class c for type int using btree as operator 1 ::;
create operator === (leftarg = int, rightarg = int, commutator = ::);

-- qualifiers are ColId, so type function name keywords aren't allowed
select 1 operator(binary.+) 2;
select 1 operator(left.+) 2;
select 1 operator(collation.+) 2;
select 1 operator(a.binary.+) 2;
create operator binary.+ (leftarg = int, rightarg = int, function = int4pl);

-- order by using takes qual_all_Op, a qualified operator needs operator(...)
select 1 from t order by a using and;
select 1 from t order by a using pg_catalog.<;

-- same for the operator of an exclusion constraint
create table t (c int, exclude using gist (c with and));
create table t (c int, exclude using gist (c with binary.=));
4 changes: 4 additions & 0 deletions crates/squawk_parser/tests/data/ok/create_table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ create table t (
exclude using btree ( a with f.buzz.> ) where ( a > 10 )
);

-- exclude constraint with an explicit operator call
create table t (c int, exclude using gist (c with operator(pg_catalog.=)));
create table t (c int, exclude using gist (c with operator(=)));

-- exclude constraint multiple exclusions
create table t (
a int,
Expand Down
4 changes: 4 additions & 0 deletions crates/squawk_parser/tests/data/ok/select.sql
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,10 @@ select current_schema;
-- order_by_with_custom_op
select * from t order by a using >>>;

-- order_by_with_operator_call
select * from t order by a using operator(<);
select * from t order by a using operator(pg_catalog.<);

-- order_by_regression
SELECT sensor_id, DATE_TRUNC('day', ts) AS day, MAX(value) AS max_value, MIN(value) AS min_value
FROM sensors_uncompressed
Expand Down
106 changes: 106 additions & 0 deletions crates/squawk_parser/tests/snapshots/tests__create_table_ok.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,112 @@ SOURCE_FILE
R_PAREN ")"
SEMICOLON ";"
WHITESPACE "\n\n"
COMMENT "-- exclude constraint with an explicit operator call"
WHITESPACE "\n"
CREATE_TABLE
CREATE_KW "create"
WHITESPACE " "
TABLE_KW "table"
WHITESPACE " "
TABLE_NAME
PATH
PATH_SEGMENT
IDENT "t"
WHITESPACE " "
TABLE_ARG_LIST
L_PAREN "("
COLUMN
COLUMN_NAME
IDENT "c"
WHITESPACE " "
PATH_TYPE
PATH_REF
PATH_SEGMENT_REF
INT_KW "int"
COMMA ","
WHITESPACE " "
EXCLUDE_CONSTRAINT
EXCLUDE_KW "exclude"
WHITESPACE " "
CONSTRAINT_INDEX_METHOD
USING_KW "using"
WHITESPACE " "
ACCESS_METHOD_REF
IDENT "gist"
WHITESPACE " "
CONSTRAINT_EXCLUSION_LIST
L_PAREN "("
CONSTRAINT_EXCLUSION
NAME_REF
IDENT "c"
WHITESPACE " "
WITH_KW "with"
WHITESPACE " "
OPERATOR_CALL
OPERATOR_KW "operator"
L_PAREN "("
OP
PATH_REF
PATH_REF
PATH_SEGMENT_REF
IDENT "pg_catalog"
DOT "."
PATH_SEGMENT_REF
EQ "="
R_PAREN ")"
R_PAREN ")"
R_PAREN ")"
SEMICOLON ";"
WHITESPACE "\n"
CREATE_TABLE
CREATE_KW "create"
WHITESPACE " "
TABLE_KW "table"
WHITESPACE " "
TABLE_NAME
PATH
PATH_SEGMENT
IDENT "t"
WHITESPACE " "
TABLE_ARG_LIST
L_PAREN "("
COLUMN
COLUMN_NAME
IDENT "c"
WHITESPACE " "
PATH_TYPE
PATH_REF
PATH_SEGMENT_REF
INT_KW "int"
COMMA ","
WHITESPACE " "
EXCLUDE_CONSTRAINT
EXCLUDE_KW "exclude"
WHITESPACE " "
CONSTRAINT_INDEX_METHOD
USING_KW "using"
WHITESPACE " "
ACCESS_METHOD_REF
IDENT "gist"
WHITESPACE " "
CONSTRAINT_EXCLUSION_LIST
L_PAREN "("
CONSTRAINT_EXCLUSION
NAME_REF
IDENT "c"
WHITESPACE " "
WITH_KW "with"
WHITESPACE " "
OPERATOR_CALL
OPERATOR_KW "operator"
L_PAREN "("
OP
EQ "="
R_PAREN ")"
R_PAREN ")"
R_PAREN ")"
SEMICOLON ";"
WHITESPACE "\n\n"
COMMENT "-- exclude constraint multiple exclusions"
WHITESPACE "\n"
CREATE_TABLE
Expand Down
Loading
Loading