From 25ab79ff9b93a791d70933b460eb45f632254f29 Mon Sep 17 00:00:00 2001 From: Oliver Tan Date: Fri, 16 Jun 2023 15:15:07 +1000 Subject: [PATCH 1/2] pgrepl: add base LSN utility This commit adds a basic utility for the PG LSN functionality. Release note: None --- pkg/sql/pgrepl/lsn/BUILD.bazel | 11 +++++++++++ pkg/sql/pgrepl/lsn/lsn.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 pkg/sql/pgrepl/lsn/BUILD.bazel create mode 100644 pkg/sql/pgrepl/lsn/lsn.go diff --git a/pkg/sql/pgrepl/lsn/BUILD.bazel b/pkg/sql/pgrepl/lsn/BUILD.bazel new file mode 100644 index 000000000000..69311185c358 --- /dev/null +++ b/pkg/sql/pgrepl/lsn/BUILD.bazel @@ -0,0 +1,11 @@ +load("//build/bazelutil/unused_checker:unused.bzl", "get_x_data") +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "lsn", + srcs = ["lsn.go"], + importpath = "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/lsn", + visibility = ["//visibility:public"], +) + +get_x_data(name = "get_x_data") diff --git a/pkg/sql/pgrepl/lsn/lsn.go b/pkg/sql/pgrepl/lsn/lsn.go new file mode 100644 index 000000000000..460b6007d83b --- /dev/null +++ b/pkg/sql/pgrepl/lsn/lsn.go @@ -0,0 +1,28 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +// Package lsn contains logic for handling the pg_lsn type. +package lsn + +import "fmt" + +type LSN uint64 + +func (lsn LSN) String() string { + return fmt.Sprintf("%X/%X", uint32(lsn>>32), uint32(lsn)) +} + +func ParseLSN(str string) (LSN, error) { + var lo, hi uint32 + if _, err := fmt.Sscanf(str, "%X/%X", &hi, &lo); err != nil { + return 0, err + } + return (LSN(hi) << 32) | LSN(lo), nil +} From 66c354edacafb325e33c419707ad6b9ca39e87de Mon Sep 17 00:00:00 2001 From: Oliver Tan Date: Mon, 19 Jun 2023 13:47:41 +1000 Subject: [PATCH 2/2] pgrepl: implement the parser for the PostgreSQL replication protocol This commit implements parsing the [PG replication protocol syntax](https://www.postgresql.org/docs/current/protocol-replication.html) which is derived from the `.y` file from PG itself. We had to invent some new lexing protocols as the lexing mechanism is different to standard plpgsql/pgsql. This is in preparation for supporting pglogical within CRDB. Release note: None --- Makefile | 32 +- pkg/BUILD.bazel | 5 + pkg/gen/misc.bzl | 1 + pkg/sql/lexbase/sql-gen.sh | 2 +- pkg/sql/pgrepl/lsn/BUILD.bazel | 3 - pkg/sql/pgrepl/pgreplparser/.gitignore | 6 + pkg/sql/pgrepl/pgreplparser/BUILD.bazel | 78 +++ pkg/sql/pgrepl/pgreplparser/lexer.go | 306 +++++++++++ pkg/sql/pgrepl/pgreplparser/lexer_test.go | 74 +++ pkg/sql/pgrepl/pgreplparser/parser.go | 31 ++ pkg/sql/pgrepl/pgreplparser/parser_test.go | 77 +++ pkg/sql/pgrepl/pgreplparser/pgrepl.y | 506 ++++++++++++++++++ pkg/sql/pgrepl/pgreplparser/pgreplparser.go | 12 + .../testdata/lexer/identifiers.ddt | 25 + .../pgreplparser/testdata/lexer/keywords.ddt | 41 ++ .../pgreplparser/testdata/lexer/lsn.ddt | 47 ++ .../pgreplparser/testdata/lexer/numbers.ddt | 25 + .../pgreplparser/testdata/lexer/quotes.ddt | 40 ++ .../testdata/parser/base_backup.ddt | 25 + .../parser/create_replication_slot.ddt | 55 ++ .../testdata/parser/drop_replication_slot.ddt | 11 + .../testdata/parser/identify_system.ddt | 19 + .../testdata/parser/read_replication_slot.ddt | 5 + .../testdata/parser/start_replication.ddt | 79 +++ .../testdata/parser/timeline_history.ddt | 32 ++ pkg/sql/pgrepl/pgrepltree/BUILD.bazel | 23 + pkg/sql/pgrepl/pgrepltree/base_backup.go | 49 ++ .../pgrepltree/create_replication_slot.go | 70 +++ .../pgrepltree/drop_replication_slot.go | 49 ++ pkg/sql/pgrepl/pgrepltree/identify_system.go | 43 ++ pkg/sql/pgrepl/pgrepltree/option.go | 37 ++ pkg/sql/pgrepl/pgrepltree/pgrepltree.go | 19 + .../pgrepltree/read_replication_slot.go | 45 ++ pkg/sql/pgrepl/pgrepltree/replication_slot.go | 18 + .../pgrepl/pgrepltree/start_replication.go | 76 +++ pkg/sql/pgrepl/pgrepltree/timeline_history.go | 45 ++ .../sem/tree/statementreturntype_string.go | 5 +- pkg/sql/sem/tree/stmt.go | 2 + 38 files changed, 2012 insertions(+), 6 deletions(-) create mode 100644 pkg/sql/pgrepl/pgreplparser/.gitignore create mode 100644 pkg/sql/pgrepl/pgreplparser/BUILD.bazel create mode 100644 pkg/sql/pgrepl/pgreplparser/lexer.go create mode 100644 pkg/sql/pgrepl/pgreplparser/lexer_test.go create mode 100644 pkg/sql/pgrepl/pgreplparser/parser.go create mode 100644 pkg/sql/pgrepl/pgreplparser/parser_test.go create mode 100644 pkg/sql/pgrepl/pgreplparser/pgrepl.y create mode 100644 pkg/sql/pgrepl/pgreplparser/pgreplparser.go create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/lexer/identifiers.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/lexer/keywords.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/lexer/lsn.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/lexer/numbers.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/lexer/quotes.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/parser/base_backup.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/parser/create_replication_slot.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/parser/drop_replication_slot.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/parser/identify_system.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/parser/read_replication_slot.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/parser/start_replication.ddt create mode 100644 pkg/sql/pgrepl/pgreplparser/testdata/parser/timeline_history.ddt create mode 100644 pkg/sql/pgrepl/pgrepltree/BUILD.bazel create mode 100644 pkg/sql/pgrepl/pgrepltree/base_backup.go create mode 100644 pkg/sql/pgrepl/pgrepltree/create_replication_slot.go create mode 100644 pkg/sql/pgrepl/pgrepltree/drop_replication_slot.go create mode 100644 pkg/sql/pgrepl/pgrepltree/identify_system.go create mode 100644 pkg/sql/pgrepl/pgrepltree/option.go create mode 100644 pkg/sql/pgrepl/pgrepltree/pgrepltree.go create mode 100644 pkg/sql/pgrepl/pgrepltree/read_replication_slot.go create mode 100644 pkg/sql/pgrepl/pgrepltree/replication_slot.go create mode 100644 pkg/sql/pgrepl/pgrepltree/start_replication.go create mode 100644 pkg/sql/pgrepl/pgrepltree/timeline_history.go diff --git a/Makefile b/Makefile index 4a6711c86baf..37befde0bd44 100644 --- a/Makefile +++ b/Makefile @@ -782,6 +782,7 @@ SQLPARSER_TARGETS = \ pkg/sql/lexbase/tokens.go \ pkg/sql/lexbase/keywords.go \ pkg/sql/lexbase/reserved_keywords.go \ + pkg/sql/pgrepl/pgreplparser/pgrepl.go \ pkg/sql/plpgsql/parser/plpgsql.go \ pkg/sql/plpgsql/parser/lexbase/tokens.go \ pkg/sql/plpgsql/parser/lexbase/keywords.go \ @@ -1516,6 +1517,14 @@ pkg/sql/plpgsql/parser/gen/plpgsql.go.tmp: pkg/sql/plpgsql/parser/gen/plpgsql-ge echo "$$ret"; exit 1; \ fi +.SECONDARY: pkg/sql/pgrepl/pgreplparser/gen/pgrepl.go.tmp +pkg/sql/pgrepl/pgreplparser/gen/pgrepl.go.tmp: pkg/sql/pgrepl/pgreplparser/gen/pgrepl-gen.y bin/.bootstrap + set -euo pipefail; \ + ret=$$(cd pkg/sql/pgrepl/pgreplparser/gen && goyacc -p pgrepl -o pgrepl.go.tmp pgrepl-gen.y); \ + if expr "$$ret" : ".*conflicts" >/dev/null; then \ + echo "$$ret"; exit 1; \ + fi + pkg/sql/plpgsql/parser/lexbase/tokens.go: pkg/sql/plpgsql/parser/gen/plpgsql.go.tmp (echo "// Code generated by make. DO NOT EDIT."; \ echo "// GENERATED FILE DO NOT EDIT"; \ @@ -1534,6 +1543,13 @@ pkg/sql/plpgsql/parser/plpgsql.go: pkg/sql/plpgsql/parser/gen/plpgsql.go.tmp | b mv -f $@.tmp $@ goimports -w $@ +pkg/sql/pgrepl/pgreplparser/pgrepl.go: pkg/sql/pgrepl/pgreplparser/gen/pgrepl.go.tmp | bin/.bootstrap + (echo "// Code generated by goyacc. DO NOT EDIT."; \ + echo "// GENERATED FILE DO NOT EDIT"; \ + cat $^) > $@.tmp || rm $@.tmp + mv -f $@.tmp $@ + goimports -w $@ + # This modifies the grammar to: # - improve the types used by the generated parser for non-terminals # - expand the help rules. @@ -1586,6 +1602,20 @@ pkg/sql/plpgsql/parser/gen/plpgsql-gen.y: pkg/sql/plpgsql/parser/plpgsql.y mv -f $@.tmp $@ rm pkg/sql/plpgsql/parser/gen/types_regex.tmp + +.SECONDARY: pkg/sql/pgrepl/pgreplparser/gen/pgrepl-gen.y +pkg/sql/pgrepl/pgreplparser/gen/pgrepl-gen.y: pkg/sql/pgrepl/pgreplparser/pgrepl.y + mkdir -p pkg/sql/pgrepl/pgreplparser/gen + set -euo pipefail; \ + awk '/func.*pgreplSymUnion/ {print $$(NF - 1)}' pkg/sql/pgrepl/pgreplparser/pgrepl.y | \ + sed -e 's/[]\/$$*.^|[]/\\&/g' | \ + sed -e "s/^/s_(type|token) <(/" | \ + awk '{print $$0")>_\\1 /* <\\2> */_"}' > pkg/sql/pgrepl/pgreplparser/gen/types_regex.tmp; \ + sed -E -f pkg/sql/pgrepl/pgreplparser/gen/types_regex.tmp < pkg/sql/pgrepl/pgreplparser/pgrepl.y | \ + sed -Ee 's,//.*$$,,g;s,/[*]([^*]|[*][^/])*[*]/, ,g;s/ +$$//g' > $@.tmp || rm $@.tmp + mv -f $@.tmp $@ + rm pkg/sql/pgrepl/pgreplparser/gen/types_regex.tmp + pkg/sql/plpgsql/parser/lexbase/keywords.go: pkg/sql/plpgsql/parser/plpgsql.y pkg/sql/lexbase/allkeywords/main.go | bin/.bootstrap $(GO) run -tags all-keywords pkg/sql/lexbase/allkeywords/main.go < $< > $@.tmp || rm $@.tmp mv -f $@.tmp $@ @@ -1766,7 +1796,7 @@ cleanshort: -$(GO) clean $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LINKFLAGS)' -i github.com/cockroachdb/cockroach... $(FIND_RELEVANT) -type f -name '*.test' -exec rm {} + for f in cockroach*; do if [ -f "$$f" ]; then rm "$$f"; fi; done - rm -rf pkg/sql/parser/gen pkg/sql/plpgsql/parser/gen + rm -rf pkg/sql/parser/gen pkg/sql/plpgsql/parser/gen pkg/sql/pgrepl/pgreplparser/gen .PHONY: clean clean: ## Like cleanshort, but also includes C++ artifacts, Bazel artifacts, and the go build cache. diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel index 092af692bd85..a326fdfb2ab0 100644 --- a/pkg/BUILD.bazel +++ b/pkg/BUILD.bazel @@ -483,6 +483,7 @@ ALL_TESTS = [ "//pkg/sql/opt:opt_test", "//pkg/sql/parser:parser_disallowed_imports_test", "//pkg/sql/parser:parser_test", + "//pkg/sql/pgrepl/pgreplparser:pgreplparser_test", "//pkg/sql/pgwire/hba:hba_test", "//pkg/sql/pgwire/identmap:identmap_test", "//pkg/sql/pgwire/pgerror:pgerror_test", @@ -1858,6 +1859,10 @@ GO_TARGETS = [ "//pkg/sql/parser/statements:statements", "//pkg/sql/parser:parser", "//pkg/sql/parser:parser_test", + "//pkg/sql/pgrepl/lsn:lsn", + "//pkg/sql/pgrepl/pgreplparser:pgreplparser", + "//pkg/sql/pgrepl/pgreplparser:pgreplparser_test", + "//pkg/sql/pgrepl/pgrepltree:pgrepltree", "//pkg/sql/pgwire/hba:hba", "//pkg/sql/pgwire/hba:hba_test", "//pkg/sql/pgwire/identmap:identmap", diff --git a/pkg/gen/misc.bzl b/pkg/gen/misc.bzl index b6ef4c700c34..53fa6a64ddef 100644 --- a/pkg/gen/misc.bzl +++ b/pkg/gen/misc.bzl @@ -14,6 +14,7 @@ MISC_SRCS = [ "//pkg/roachprod/vm/aws:terraform/main.tf", "//pkg/spanconfig/spanconfigstore:entry_interval_btree.go", "//pkg/spanconfig/spanconfigstore:entry_interval_btree_test.go", + "//pkg/sql/pgrepl/pgreplparser:pgrepl.go", "//pkg/sql/plpgsql/parser/lexbase:keywords.go", "//pkg/sql/plpgsql/parser/lexbase:tokens.go", "//pkg/sql/plpgsql/parser:plpgsql.go", diff --git a/pkg/sql/lexbase/sql-gen.sh b/pkg/sql/lexbase/sql-gen.sh index 163e83d5b14a..ce891e32c1eb 100755 --- a/pkg/sql/lexbase/sql-gen.sh +++ b/pkg/sql/lexbase/sql-gen.sh @@ -17,7 +17,7 @@ GENYACC=$LANG-gen.y awk '{print $0")>_\\1 /* <\\2> */_"}' > types_regex.tmp sed -E -f types_regex.tmp < $1 | \ - if [ $LANG != plpgsql ]; then \ + if [ $LANG != plpgsql ] && [ $LANG != pgrepl ]; then \ awk -f $3 | \ sed -Ee 's,//.*$$,,g;s,/[*]([^*]|[*][^/])*[*]/, ,g;s/ +$$//g' > $GENYACC else diff --git a/pkg/sql/pgrepl/lsn/BUILD.bazel b/pkg/sql/pgrepl/lsn/BUILD.bazel index 69311185c358..5635e0838e11 100644 --- a/pkg/sql/pgrepl/lsn/BUILD.bazel +++ b/pkg/sql/pgrepl/lsn/BUILD.bazel @@ -1,4 +1,3 @@ -load("//build/bazelutil/unused_checker:unused.bzl", "get_x_data") load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( @@ -7,5 +6,3 @@ go_library( importpath = "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/lsn", visibility = ["//visibility:public"], ) - -get_x_data(name = "get_x_data") diff --git a/pkg/sql/pgrepl/pgreplparser/.gitignore b/pkg/sql/pgrepl/pgreplparser/.gitignore new file mode 100644 index 000000000000..1790eb07f557 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/.gitignore @@ -0,0 +1,6 @@ +# Do not add environment-specific entries here (see the top-level .gitignore +# for reasoning and alternatives). + +pgrepl.go +y.output +gen diff --git a/pkg/sql/pgrepl/pgreplparser/BUILD.bazel b/pkg/sql/pgrepl/pgreplparser/BUILD.bazel new file mode 100644 index 000000000000..d306c7808ffb --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/BUILD.bazel @@ -0,0 +1,78 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +sh_binary( + name = "pgrepl-gen", + srcs = ["//pkg/sql/lexbase:sql-gen.sh"], +) + +# Define the target to auto-generate sql.go from the grammar file. +genrule( + name = "pgrepl-goyacc", + srcs = [ + "pgrepl.y", + ], + outs = ["pgrepl.go"], + cmd = """ + export GOPATH=/nonexist-gopath + $(location :pgrepl-gen) $(location pgrepl.y) pgrepl ""\ + $(location pgrepl.go) $(location @org_golang_x_tools//cmd/goyacc) \ + $(location @com_github_cockroachdb_gostdlib//x/tools/cmd/goimports) \ + + """, + exec_tools = [ + ":pgrepl-gen", + "@com_github_cockroachdb_gostdlib//x/tools/cmd/goimports", + "@org_golang_x_tools//cmd/goyacc", + ], + visibility = ["//visibility:public"], +) + +go_library( + name = "pgreplparser", + srcs = [ + "lexer.go", + "parser.go", + "pgrepl.go", + "pgreplparser.go", + ], + importpath = "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/pgreplparser", + visibility = ["//visibility:public"], + deps = [ + "//pkg/sql/lexbase", + "//pkg/sql/parser", + "//pkg/sql/pgrepl/lsn", + "//pkg/sql/pgrepl/pgrepltree", + "//pkg/sql/pgwire/pgcode", + "//pkg/sql/pgwire/pgerror", + "//pkg/sql/sem/tree", + "@com_github_cockroachdb_errors//:errors", + "@com_github_cockroachdb_redact//:redact", # keep + ], +) + +exports_files( + [ + "pgrepl.y", + ], + visibility = ["//visibility:public"], +) + +go_test( + name = "pgreplparser_test", + srcs = [ + "lexer_test.go", + "parser_test.go", + ], + args = ["-test.timeout=295s"], + data = glob(["testdata/**"]), + embed = [":pgreplparser"], + deps = [ + "//pkg/sql/pgrepl/lsn", + "//pkg/sql/pgwire/pgerror", + "//pkg/sql/sem/tree", + "//pkg/testutils/datapathutils", + "@com_github_cockroachdb_datadriven//:datadriven", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/pkg/sql/pgrepl/pgreplparser/lexer.go b/pkg/sql/pgrepl/pgreplparser/lexer.go new file mode 100644 index 000000000000..63494e1d3086 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/lexer.go @@ -0,0 +1,306 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgreplparser + +import ( + "fmt" + "go/constant" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/cockroachdb/cockroach/pkg/sql/lexbase" + "github.com/cockroachdb/cockroach/pkg/sql/parser" + "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/lsn" + "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/pgrepltree" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/errors" +) + +const ( + singleQuote = '\'' + doubleQuote = '"' +) + +// lexer implements the Lexer goyacc interface. +// It differs from the sql/scanner package as replication has unique, case +// sensitive behavior with unique keywords. +// See: https://github.com/postgres/postgres/blob/4327f6c7480fea9348ea6825a9d38a71b2ef8785/src/backend/replication/repl_scanner.l +type lexer struct { + in string + pos int + + lastToken pgreplSymType + lastError error + stmt pgrepltree.ReplicationStatement +} + +func newLexer(str string) *lexer { + return &lexer{in: str} +} + +func (l *lexer) Lex(lval *pgreplSymType) int { + if l.done() { + lval.SetID(0) + lval.SetPos(int32(len(l.in))) + lval.SetStr("EOF") + return 0 + } + l.lex(lval) + l.lastToken = *lval + return int(lval.id) +} + +func (l *lexer) Error(e string) { + e = strings.TrimPrefix(e, "syntax error: ") // we'll add it again below. + err := pgerror.WithCandidateCode(errors.Newf("%s", e), pgcode.Syntax) + lastTok := l.lastToken + l.lastError = parser.PopulateErrorDetails(lastTok.id, lastTok.str, lastTok.pos, err, l.in) +} + +func (l *lexer) lex(lval *pgreplSymType) { + l.skipSpace() + + lval.SetUnionVal(nil) + startPos := l.pos + lval.SetPos(int32(startPos)) + ch := l.next() + switch { + case ch == singleQuote: + l.scanUntilEndQuote(lval, singleQuote, nil, SCONST) + case ch == doubleQuote: + l.scanUntilEndQuote(lval, doubleQuote, lexbase.NormalizeString, IDENT) + case unicode.IsDigit(ch): + curPos := l.pos + scanNumLoop: + for curPos < len(l.in) { + nextCh, sz := utf8.DecodeRuneInString(l.in[curPos:]) + switch { + case unicode.IsDigit(nextCh): + // We are either a number or part of a LSN. + // We only enter the LSN check once we detect a forward slash. + case isHexDigit(nextCh) || nextCh == '/': + // Any presence of a hex digit or / may make it an LSN. + if l.checkLSN(lval, startPos) { + return + } + // Otherwise, we are done - anything before this position is a number. + break scanNumLoop + default: + break scanNumLoop + } + curPos += sz + } + lval.SetStr(l.in[startPos:curPos]) + lval.SetID(UCONST) + l.pos = curPos + val, err := strconv.ParseUint(lval.Str(), 10, 64) + if err != nil { + l.lexerError(lval, err) + return + } + lval.SetUnionVal(tree.NewNumVal(constant.MakeUint64(val), lval.Str(), false)) + case isIdentifier(ch, true): + curPos := l.pos + scanChLoop: + for curPos < len(l.in) { + nextCh, sz := utf8.DecodeRuneInString(l.in[curPos:]) + switch { + case isIdentifier(nextCh, false): + case nextCh == '/': + // See if it is an LSN. + if l.checkLSN(lval, startPos) { + return + } + // Otherwise, we are done. + break scanChLoop + default: + break scanChLoop + } + curPos += sz + } + str := l.in[startPos:curPos] + id := l.getKeywordID(str) + lval.SetID(id) + if id == IDENT { + str = lexbase.NormalizeName(str) + } + lval.SetStr(str) + l.pos = curPos + default: + // Otherwise, it is the character itself. + lval.SetID(ch) + lval.SetStr(string(ch)) + } +} + +func (l *lexer) done() bool { + return l.pos >= len(l.in) +} + +const ( + identifierUnicodeMin = 200 + identifierUnicodeMax = 377 +) + +func isIdentifier(ch rune, firstCh bool) bool { + if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= identifierUnicodeMin && ch <= identifierUnicodeMax) || ch == '_' { + return true + } + return !firstCh && unicode.IsDigit(ch) +} + +func isHexDigit(nextCh rune) bool { + return (nextCh >= 'A' && nextCh <= 'F') || (nextCh >= 'a' && nextCh <= 'f') || (nextCh >= '0' && nextCh <= '9') +} + +// checkLSN checks that the current symbol is an LSN type. +// This is represented by XXXXXXXX/XXXXXXXX, where X is a hexadecimal digit. +func (l *lexer) checkLSN(lval *pgreplSymType, startPos int) bool { + curPos := startPos + + // Read up to '/'. + for curPos < len(l.in) && l.in[curPos] != '/' { + nextCh, sz := utf8.DecodeRuneInString(l.in[curPos:]) + if !isHexDigit(nextCh) { + return false + } + curPos += sz + } + + // Check we have a '/'. + if curPos < len(l.in) && l.in[curPos] != '/' { + return false + } + curPos++ + + afterSlashPos := curPos + for curPos < len(l.in) { + nextCh, sz := utf8.DecodeRuneInString(l.in[curPos:]) + if !isHexDigit(nextCh) { + break + } + curPos += sz + } + // If we haven't moved, we're not an LSN. + if afterSlashPos == curPos { + return false + } + + str := l.in[startPos:curPos] + lval.SetStr(str) + lval.SetID(RECPTR) + l.pos = curPos + lsnVal, err := lsn.ParseLSN(str) + if err != nil { + l.lexerError(lval, errors.Wrap(err, "error decoding LSN")) + return true + } + lval.SetUnionVal(lsnVal) + return true +} + +func (l *lexer) lexerError(lval *pgreplSymType, err error) { + l.lastToken = *lval + lval.id = ERROR + l.Error(err.Error()) +} + +func (l *lexer) setErr(err error) { + l.lastError = err +} + +func (l *lexer) scanUntilEndQuote( + lval *pgreplSymType, quoteCh rune, normFunc func(string) string, id int32, +) { + str := "" + for l.pos < len(l.in) { + nextCh := l.next() + // Double quotes = 1 single quote. + if nextCh == quoteCh { + if l.peek() == quoteCh { + l.next() + } else { + lval.SetID(id) + if normFunc != nil { + str = normFunc(str) + } + lval.SetStr(str) + return + } + } + str += string(nextCh) + } + lval.SetID(pgreplErrCode) + lval.SetStr(fmt.Sprintf("unfinished quote: %c", quoteCh)) +} + +func (l *lexer) peek() rune { + ch, _ := utf8.DecodeRuneInString(l.in[l.pos:]) + return ch +} + +func (l *lexer) next() rune { + ch, sz := utf8.DecodeRuneInString(l.in[l.pos:]) + l.pos += sz + return ch +} + +func (l *lexer) skipSpace() { + if inc := strings.IndexFunc(l.in[l.pos:], func(r rune) bool { return !unicode.IsSpace(r) }); inc > 0 { + l.pos += inc + } +} + +func (l *lexer) getKeywordID(str string) int32 { + switch str { + case "BASE_BACKUP": + return K_BASE_BACKUP + case "IDENTIFY_SYSTEM": + return K_IDENTIFY_SYSTEM + case "READ_REPLICATION_SLOT": + return K_READ_REPLICATION_SLOT + case "CREATE_REPLICATION_SLOT": + return K_CREATE_REPLICATION_SLOT + case "START_REPLICATION": + return K_START_REPLICATION + case "DROP_REPLICATION_SLOT": + return K_DROP_REPLICATION_SLOT + case "TIMELINE_HISTORY": + return K_TIMELINE_HISTORY + case "WAIT": + return K_WAIT + case "TIMELINE": + return K_TIMELINE + case "PHYSICAL": + return K_PHYSICAL + case "LOGICAL": + return K_LOGICAL + case "RESERVE_WAL": + return K_RESERVE_WAL + case "TEMPORARY": + return K_TEMPORARY + case "TWO_PHASE": + return K_TWO_PHASE + case "EXPORT_SNAPSHOT": + return K_EXPORT_SNAPSHOT + case "NOEXPORT_SNAPSHOT": + return K_NOEXPORT_SNAPSHOT + case "SLOT": + return K_SLOT + case "USE_SNAPSHOT": + return K_USE_SNAPSHOT + } + return IDENT +} diff --git a/pkg/sql/pgrepl/pgreplparser/lexer_test.go b/pkg/sql/pgrepl/pgreplparser/lexer_test.go new file mode 100644 index 000000000000..e4bf2ddf0646 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/lexer_test.go @@ -0,0 +1,74 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgreplparser + +import ( + "fmt" + "strings" + "testing" + + "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/lsn" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/testutils/datapathutils" + "github.com/cockroachdb/datadriven" +) + +func TestLexer(t *testing.T) { + datadriven.Walk(t, datapathutils.TestDataPath(t, "lexer"), func(t *testing.T, path string) { + datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "lex": + var sb strings.Builder + var st pgreplSymType + s := newLexer(td.Input) + var started bool + for !s.done() { + if started { + sb.WriteRune('\n') + } + started = true + s.lex(&st) + if s.lastError != nil { + sb.WriteString(fmt.Sprintf("ERROR: %s", s.lastError.Error())) + break + } + switch st.id { + case IDENT: + sb.WriteString("IDENT") + case SCONST: + sb.WriteString("SCONST") + case UCONST: + sb.WriteString("UCONST") + case RECPTR: + sb.WriteString("LSN") + default: + sb.WriteString(fmt.Sprintf("id:%d", st.id)) + } + sb.WriteString(fmt.Sprintf(" str:%s", st.str)) + if st.union.val != nil { + switch v := st.union.val.(type) { + case lsn.LSN: + sb.WriteString(fmt.Sprintf(" lsn:%s", v)) + case *tree.NumVal: + sb.WriteString(fmt.Sprintf(" num:%s", v)) + default: + t.Errorf("unknown union type: %T", v) + } + } + } + return sb.String() + default: + t.Errorf("unknown command %s", td.Cmd) + } + return "" + }) + }) +} diff --git a/pkg/sql/pgrepl/pgreplparser/parser.go b/pkg/sql/pgrepl/pgreplparser/parser.go new file mode 100644 index 000000000000..0de2f0d35cab --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/parser.go @@ -0,0 +1,31 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgreplparser + +import ( + "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/pgrepltree" + "github.com/cockroachdb/errors" +) + +func Parse(sql string) (pgrepltree.ReplicationStatement, error) { + lexer := newLexer(sql) + p := pgreplNewParser() + if p.Parse(lexer) != 0 { + if lexer.lastError == nil { + return nil, errors.AssertionFailedf("expected lexer error but got none") + } + return nil, lexer.lastError + } + if lexer.stmt == nil { + return nil, errors.AssertionFailedf("expected statement but got none") + } + return lexer.stmt, nil +} diff --git a/pkg/sql/pgrepl/pgreplparser/parser_test.go b/pkg/sql/pgrepl/pgreplparser/parser_test.go new file mode 100644 index 000000000000..154fda1f9525 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/parser_test.go @@ -0,0 +1,77 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgreplparser + +import ( + "bytes" + "fmt" + "testing" + + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/cockroach/pkg/testutils/datapathutils" + "github.com/cockroachdb/datadriven" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParser(t *testing.T) { + datadriven.Walk(t, datapathutils.TestDataPath(t, "parser"), func(t *testing.T, path string) { + datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string { + switch td.Cmd { + case "parse": + var expectError bool + for _, arg := range td.CmdArgs { + switch arg.Key { + case "error": + expectError = true + default: + t.Errorf("unknown cmd arg %s", arg.Key) + } + } + p, err := Parse(td.Input) + if expectError { + require.Error(t, err) + + pgerr := pgerror.Flatten(err) + msg := pgerr.Message + if pgerr.Detail != "" { + msg += "\nDETAIL: " + pgerr.Detail + } + if pgerr.Hint != "" { + msg += "\nHINT: " + pgerr.Hint + } + return msg + } + require.NoError(t, err) + ref := p.String() + note := "" + if ref != td.Input { + note = " -- normalized!" + } + var buf bytes.Buffer + fmt.Fprintf(&buf, "%s%s\n", ref, note) + constantsHidden := tree.AsStringWithFlags(p, tree.FmtHideConstants) + fmt.Fprintln(&buf, constantsHidden, "-- literals removed") + + // Test roundtrip. + reparsed, err := Parse(p.String()) + require.NoError(t, err) + assert.Equal(t, p.String(), reparsed.String()) + + return buf.String() + default: + t.Errorf("unknown command %s", td.Cmd) + } + return "" + }) + }) +} diff --git a/pkg/sql/pgrepl/pgreplparser/pgrepl.y b/pkg/sql/pgrepl/pgreplparser/pgrepl.y new file mode 100644 index 000000000000..b372d76ff664 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/pgrepl.y @@ -0,0 +1,506 @@ +%{ +// Adapted from https://github.com/postgres/postgres/blob/4327f6c7480fea9348ea6825a9d38a71b2ef8785/src/backend/replication/repl_gram.y +/*------------------------------------------------------------------------- + * + * repl_gram.y - Parser for the replication commands + * + * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/replication/repl_gram.y + * + *------------------------------------------------------------------------- + */ + +package pgreplparser + +import ( + "fmt" + + "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/lsn" + "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/pgrepltree" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + "github.com/cockroachdb/redact" +) +%} + +%{ + +func (s *pgreplSymType) ID() int32 { + return s.id +} + +func (s *pgreplSymType) SetID(id int32) { + s.id = id +} + +func (s *pgreplSymType) Pos() int32 { + return s.pos +} + +func (s *pgreplSymType) SetPos(pos int32) { + s.pos = pos +} + +func (s *pgreplSymType) Str() string { + return s.str +} + +func (s *pgreplSymType) SetStr(str string) { + s.str = str +} + +func (s *pgreplSymType) UnionVal() interface{} { + return s.union.val +} + +func (s *pgreplSymType) SetUnionVal(val interface{}) { + s.union.val = val +} + +type pgreplSymUnion struct { + val interface{} +} + +func (u *pgreplSymUnion) replicationStatement() pgrepltree.ReplicationStatement { + return u.val.(pgrepltree.ReplicationStatement) +} + +func (u *pgreplSymUnion) identifySystem() *pgrepltree.IdentifySystem { + return u.val.(*pgrepltree.IdentifySystem) +} + +func (u *pgreplSymUnion) readReplicationSlot() *pgrepltree.ReadReplicationSlot { + return u.val.(*pgrepltree.ReadReplicationSlot) +} + +func (u *pgreplSymUnion) createReplicationSlot() *pgrepltree.CreateReplicationSlot { + return u.val.(*pgrepltree.CreateReplicationSlot) +} + +func (u *pgreplSymUnion) dropReplicationSlot() *pgrepltree.DropReplicationSlot { + return u.val.(*pgrepltree.DropReplicationSlot) +} + +func (u *pgreplSymUnion) startReplication() *pgrepltree.StartReplication { + return u.val.(*pgrepltree.StartReplication) +} + +func (u *pgreplSymUnion) timelineHistory() *pgrepltree.TimelineHistory { + return u.val.(*pgrepltree.TimelineHistory) +} + +func (u *pgreplSymUnion) option() pgrepltree.Option { + return u.val.(pgrepltree.Option) +} + +func (u *pgreplSymUnion) options() pgrepltree.Options { + return u.val.(pgrepltree.Options) +} + +func (u *pgreplSymUnion) numVal() *tree.NumVal { + return u.val.(*tree.NumVal) +} + +func (u *pgreplSymUnion) expr() tree.Expr { + return u.val.(tree.Expr) +} + +func (u *pgreplSymUnion) bool() bool { + return u.val.(bool) +} + +func (u *pgreplSymUnion) lsn() lsn.LSN { + return u.val.(lsn.LSN) +} +%} + +%union { + id int32 + pos int32 + str string + union pgreplSymUnion +} + +/* Non-keyword tokens */ +%token SCONST IDENT +%token <*tree.NumVal> UCONST +%token RECPTR +%token ERROR + +/* Keyword tokens. */ +%token K_BASE_BACKUP +%token K_IDENTIFY_SYSTEM +%token K_READ_REPLICATION_SLOT +//%token K_SHOW +%token K_START_REPLICATION +%token K_CREATE_REPLICATION_SLOT +%token K_DROP_REPLICATION_SLOT +%token K_TIMELINE_HISTORY +%token K_WAIT +%token K_TIMELINE +%token K_PHYSICAL +%token K_LOGICAL +%token K_SLOT +%token K_RESERVE_WAL +%token K_TEMPORARY +%token K_TWO_PHASE +%token K_EXPORT_SNAPSHOT +%token K_NOEXPORT_SNAPSHOT +%token K_USE_SNAPSHOT + +%type cmd command +%type base_backup start_replication start_logical_replication + create_replication_slot drop_replication_slot identify_system + read_replication_slot timeline_history + // show +%type generic_option_list +%type generic_option +%type <*tree.NumVal> opt_timeline +%type plugin_options plugin_opt_list +%type plugin_opt_elem +%type plugin_opt_arg +%type opt_slot ident_or_keyword +%type opt_temporary +%type create_slot_options create_slot_legacy_opt_list +%type create_slot_legacy_opt + +%% + +cmd: command opt_semicolon + { + pgrepllex.(*lexer).stmt = $1.replicationStatement() + } + ; + +opt_semicolon: ';' + | /* EMPTY */ + ; + +command: + identify_system + | base_backup + | start_replication + | start_logical_replication + | create_replication_slot + | drop_replication_slot + | read_replication_slot + | timeline_history + ; + +/* + * IDENTIFY_SYSTEM + */ +identify_system: + K_IDENTIFY_SYSTEM + { + $$.val = &pgrepltree.IdentifySystem{} + } + ; + +/* + * READ_REPLICATION_SLOT %s + */ +read_replication_slot: + /* NOTE(otan): pg has var_name instead of IDENT, but is unclear why */ + K_READ_REPLICATION_SLOT IDENT + { + $$.val = &pgrepltree.ReadReplicationSlot{ + Slot: tree.Name($2), + } + } + ; + +/* + * SHOW setting + * NOTE(otan): we omit K_SHOW as the fallback from being parsed by + * the replication protocol is to use the normal SQL protocol. + * The normal SQL protocol already handles SHOW. +show: + K_SHOW var_name + { + VariableShowStmt *n = makeNode(VariableShowStmt); + n->name = $2; + $$ = (Node *) n; + } + +var_name: IDENT { $$ = $1; } + | var_name '.' IDENT + { $$ = fmt.Sprintf("%s.%s", $1, $3); } + ; +*/ + +/* + * BASE_BACKUP [ ( option [ 'value' ] [, ...] ) ] + */ +base_backup: + K_BASE_BACKUP '(' generic_option_list ')' + { + $$.val = &pgrepltree.BaseBackup{ + Options: $3.options(), + } + } + | K_BASE_BACKUP + { + $$.val = &pgrepltree.BaseBackup{} + } + ; + +create_replication_slot: + /* CREATE_REPLICATION_SLOT slot [TEMPORARY] PHYSICAL [options] */ + K_CREATE_REPLICATION_SLOT IDENT opt_temporary K_PHYSICAL create_slot_options + { + $$.val = &pgrepltree.CreateReplicationSlot{ + Slot: tree.Name($2), + Temporary: $3.bool(), + Kind: pgrepltree.PhysicalReplication, + Options: $5.options(), + } + } + /* CREATE_REPLICATION_SLOT slot [TEMPORARY] LOGICAL plugin [options] */ + | K_CREATE_REPLICATION_SLOT IDENT opt_temporary K_LOGICAL IDENT create_slot_options + { + $$.val = &pgrepltree.CreateReplicationSlot{ + Slot: tree.Name($2), + Temporary: $3.bool(), + Kind: pgrepltree.LogicalReplication, + Plugin: tree.Name($5), + Options: $6.options(), + } + } + ; + +create_slot_options: + '(' generic_option_list ')' { $$ = $2; } + | create_slot_legacy_opt_list { $$ = $1; } + ; + +create_slot_legacy_opt_list: + create_slot_legacy_opt_list create_slot_legacy_opt + { $$.val = append($1.options(), $2.option()) } + | /* EMPTY */ + { $$.val = pgrepltree.Options(nil); } + ; + +create_slot_legacy_opt: + K_EXPORT_SNAPSHOT + { + $$.val = pgrepltree.Option{ + Key: tree.Name("snapshot"), + Value: tree.NewStrVal("export"), + } + } + | K_NOEXPORT_SNAPSHOT + { + $$.val = pgrepltree.Option{ + Key: tree.Name("snapshot"), + Value: tree.NewStrVal("nothing"), + } + } + | K_USE_SNAPSHOT + { + $$.val = pgrepltree.Option{ + Key: tree.Name("snapshot"), + Value: tree.NewStrVal("use"), + } + } + | K_RESERVE_WAL + { + $$.val = pgrepltree.Option{ + Key: tree.Name("reserve_wal"), + Value: tree.NewStrVal("true"), + } + } + | K_TWO_PHASE + { + $$.val = pgrepltree.Option{ + Key: tree.Name("two_phase"), + Value: tree.NewStrVal("true"), + } + } + ; + +/* DROP_REPLICATION_SLOT slot */ +drop_replication_slot: + K_DROP_REPLICATION_SLOT IDENT + { + $$.val = &pgrepltree.DropReplicationSlot{ + Slot: tree.Name($2), + } + } + | K_DROP_REPLICATION_SLOT IDENT K_WAIT + { + $$.val = &pgrepltree.DropReplicationSlot{ + Slot: tree.Name($2), + Wait: true, + } + } + ; + +/* + * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %d] + */ +start_replication: + K_START_REPLICATION opt_slot opt_physical RECPTR opt_timeline + { + ret := &pgrepltree.StartReplication{ + Slot: tree.Name($2), + Kind: pgrepltree.PhysicalReplication, + LSN: $4.lsn(), + } + if $5.val != nil { + ret.Timeline = $5.numVal() + } + $$.val = ret + } + ; + +/* START_REPLICATION SLOT slot LOGICAL %X/%X options */ +start_logical_replication: + K_START_REPLICATION K_SLOT IDENT K_LOGICAL RECPTR plugin_options + { + $$.val = &pgrepltree.StartReplication{ + Slot: tree.Name($3), + Kind: pgrepltree.LogicalReplication, + LSN: $5.lsn(), + Options: $6.options(), + } + } + ; +/* + * TIMELINE_HISTORY %d + */ +timeline_history: + K_TIMELINE_HISTORY UCONST + { + if i, err := $2.numVal().AsInt64(); err != nil || uint64(i) <= 0 { + pgrepllex.(*lexer).setErr(pgerror.Newf(pgcode.Syntax, "expected a positive integer for timeline")) + return 1 + } + $$.val = &pgrepltree.TimelineHistory{Timeline: $2.numVal()} + } + ; + +opt_physical: + K_PHYSICAL + | /* EMPTY */ + ; + +opt_temporary: + K_TEMPORARY { $$.val = true; } + | /* EMPTY */ { $$.val = false; } + ; + +opt_slot: + K_SLOT IDENT + { $$ = $2; } + | /* EMPTY */ + { $$ = ""; } + ; + +opt_timeline: + K_TIMELINE UCONST + { + if i, err := $2.numVal().AsInt64(); err != nil || uint64(i) <= 0 { + pgrepllex.(*lexer).setErr(pgerror.Newf(pgcode.Syntax, "expected a positive integer for timeline")) + return 1 + } + $$.val = $2.numVal(); + } + | /* EMPTY */ { $$.val = nil; } + ; + + +plugin_options: + '(' plugin_opt_list ')' { $$ = $2; } + | /* EMPTY */ { $$.val = pgrepltree.Options(nil); } + ; + +plugin_opt_list: + plugin_opt_elem + { + $$.val = pgrepltree.Options{$1.option()} + } + | plugin_opt_list ',' plugin_opt_elem + { + $$.val = append($1.options(), $3.option()) + } + ; + +plugin_opt_elem: + IDENT plugin_opt_arg + { + $$.val = pgrepltree.Option{ + Key: tree.Name($1), + Value: $2.expr(), + } + } + ; + +plugin_opt_arg: + SCONST { $$.val = tree.NewStrVal($1); } + | /* EMPTY */ { $$.val = tree.Expr(nil); } + ; + +generic_option_list: + generic_option_list ',' generic_option + { $$.val = append($1.options(), $3.option()); } + | generic_option + { $$.val = pgrepltree.Options{$1.option()} } + ; + +generic_option: + ident_or_keyword + { + $$.val = pgrepltree.Option{Key: tree.Name($1)} + } + | ident_or_keyword IDENT + { + $$.val = pgrepltree.Option{ + Key: tree.Name($1), + Value: tree.NewStrVal($2), + } + } + | ident_or_keyword SCONST + { + $$.val = pgrepltree.Option{ + Key: tree.Name($1), + Value: tree.NewStrVal($2), + } + } + | ident_or_keyword UCONST + { + $$.val = pgrepltree.Option{ + Key: tree.Name($1), + Value: $2.numVal(), + } + } + ; + +ident_or_keyword: + IDENT { $$ = $1; } + | K_BASE_BACKUP { $$ = "base_backup"; } + | K_IDENTIFY_SYSTEM { $$ = "identify_system"; } + //| K_SHOW { $$ = "show"; } + | K_START_REPLICATION { $$ = "start_replication"; } + | K_CREATE_REPLICATION_SLOT { $$ = "create_replication_slot"; } + | K_DROP_REPLICATION_SLOT { $$ = "drop_replication_slot"; } + | K_TIMELINE_HISTORY { $$ = "timeline_history"; } + | K_WAIT { $$ = "wait"; } + | K_TIMELINE { $$ = "timeline"; } + | K_PHYSICAL { $$ = "physical"; } + | K_LOGICAL { $$ = "logical"; } + | K_SLOT { $$ = "slot"; } + | K_RESERVE_WAL { $$ = "reserve_wal"; } + | K_TEMPORARY { $$ = "temporary"; } + | K_TWO_PHASE { $$ = "two_phase"; } + | K_EXPORT_SNAPSHOT { $$ = "export_snapshot"; } + | K_NOEXPORT_SNAPSHOT { $$ = "noexport_snapshot"; } + | K_USE_SNAPSHOT { $$ = "use_snapshot"; } + ; + +%% diff --git a/pkg/sql/pgrepl/pgreplparser/pgreplparser.go b/pkg/sql/pgrepl/pgreplparser/pgreplparser.go new file mode 100644 index 000000000000..3f0ecdb474d5 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/pgreplparser.go @@ -0,0 +1,12 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +// Package pgreplparser contains methods for parsing replication related SQL. +package pgreplparser diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/lexer/identifiers.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/identifiers.ddt new file mode 100644 index 000000000000..0645354422d4 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/identifiers.ddt @@ -0,0 +1,25 @@ +lex +aSDFG_h +---- +IDENT str:asdfg_h + +lex +a000120_SDFA +---- +IDENT str:a000120_sdfa + +lex +Éa000120_SDFAÉ +---- +IDENT str:éa000120_sdfaé + +lex +1Éa000120_SDFAÉ +---- +UCONST str:1 num:1 +IDENT str:éa000120_sdfaé + +lex +"BASE_BACKUP" +---- +IDENT str:BASE_BACKUP diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/lexer/keywords.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/keywords.ddt new file mode 100644 index 000000000000..0a9d9d9c4a8d --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/keywords.ddt @@ -0,0 +1,41 @@ +lex +START_REPLICATION SLOT a LOGICAL aa/00 +---- +id:57354 str:START_REPLICATION +id:57362 str:SLOT +IDENT str:a +id:57361 str:LOGICAL +LSN str:aa/00 lsn:AA/0 + +lex +CREATE_REPLICATION_SLOT +---- +id:57355 str:CREATE_REPLICATION_SLOT + +lex +DROP_REPLICATION_SLOT +---- +id:57356 str:DROP_REPLICATION_SLOT + +lex +READ_REPLICATION_SLOT +---- +id:57353 str:READ_REPLICATION_SLOT + +lex +IDENTIFY_SYSTEM; +---- +id:57352 str:IDENTIFY_SYSTEM +id:59 str:; + +# use identifier if case does not match +lex +iDENTIFY_SYSTEM +---- +IDENT str:identify_system + +lex +BASE_BACKUP NOEXPORT_SNAPSHOT +---- +id:57351 str:BASE_BACKUP +id:57367 str:NOEXPORT_SNAPSHOT diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/lexer/lsn.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/lsn.ddt new file mode 100644 index 000000000000..29aa0baa2c5a --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/lsn.ddt @@ -0,0 +1,47 @@ +lex +0/0 +---- +LSN str:0/0 lsn:0/0 + +lex +0f/A0 +---- +LSN str:0f/A0 lsn:F/A0 + +lex +A0/f +---- +LSN str:A0/f lsn:A0/F + +lex +0A0FF0/ea0F +---- +LSN str:0A0FF0/ea0F lsn:A0FF0/EA0F + +lex +bA1010/f +---- +LSN str:bA1010/f lsn:BA1010/F + +# invalid character +lex +00g/a1 +---- +UCONST str:00 num:00 +IDENT str:g +id:47 str:/ +IDENT str:a1 + +# ends with a / +lex +00ff/ +---- +UCONST str:00 num:00 +IDENT str:ff +id:47 str:/ + +# too big +lex +bA101001010101010/f +---- +ERROR: at or near "bA101001010101010/f": syntax error: error decoding LSN: strconv.ParseUint: parsing "bA101001010101010": value out of range diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/lexer/numbers.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/numbers.ddt new file mode 100644 index 000000000000..412b076d1773 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/numbers.ddt @@ -0,0 +1,25 @@ +lex +34234 +---- +UCONST str:34234 num:34234 + +# negative +lex +-242141242 +---- +id:45 str:- +UCONST str:242141242 num:242141242 + +# decimal +lex +134213213.13 +---- +UCONST str:134213213 num:134213213 +id:46 str:. +UCONST str:13 num:13 + +# not a uint64 +lex +348329048329483290483290490327489237895723578942758428957248954289348905245784209584209580429589024809524 +---- +ERROR: at or near "348329048329483290483290490327489237895723578942758428957248954289348905245784209584209580429589024809524": syntax error: strconv.ParseUint: parsing "348329048329483290483290490327489237895723578942758428957248954289348905245784209584209580429589024809524": value out of range diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/lexer/quotes.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/quotes.ddt new file mode 100644 index 000000000000..55afcc88f6eb --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/lexer/quotes.ddt @@ -0,0 +1,40 @@ +lex +'' +---- +SCONST str: + +lex +'IDENTIFY_SYSTEM' +---- +SCONST str:IDENTIFY_SYSTEM + +lex +'a''a' +---- +SCONST str:a'a + +lex +"" +---- +IDENT str: + +lex +"IDENTIFY_SYSTEM" +---- +IDENT str:IDENTIFY_SYSTEM + +lex +"a""a" +---- +IDENT str:a"a + +# unfinished quotes +lex +'abc +---- +id:2 str:unfinished quote: ' + +lex +"abc +---- +id:2 str:unfinished quote: " diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/parser/base_backup.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/parser/base_backup.ddt new file mode 100644 index 000000000000..56382bcd0176 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/parser/base_backup.ddt @@ -0,0 +1,25 @@ +parse error +base_BACKUP +---- +at or near "base_backup": syntax error +DETAIL: source SQL: +base_BACKUP +^ + +parse +BASE_BACKUP +---- +BASE_BACKUP +BASE_BACKUP -- literals removed + +parse +BASE_BACKUP ( single_option ) +---- +BASE_BACKUP (single_option) -- normalized! +BASE_BACKUP (single_option) -- literals removed + +parse +BASE_BACKUP ( BASE_BACKUP "a", someword 'b', someint 123, word ) +---- +BASE_BACKUP (base_backup 'a', someword 'b', someint 123, word) -- normalized! +BASE_BACKUP (base_backup '_', someword '_', someint _, word) -- literals removed diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/parser/create_replication_slot.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/parser/create_replication_slot.ddt new file mode 100644 index 000000000000..b15cb146cab5 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/parser/create_replication_slot.ddt @@ -0,0 +1,55 @@ +parse +CREATE_REPLICATION_SLOT "slot_Name1" PHYSICAL +---- +CREATE_REPLICATION_SLOT "slot_Name1" PHYSICAL +CREATE_REPLICATION_SLOT "slot_Name1" PHYSICAL -- literals removed + +parse +CREATE_REPLICATION_SLOT "slot_Name1" PHYSICAL EXPORT_SNAPSHOT +---- +CREATE_REPLICATION_SLOT "slot_Name1" PHYSICAL (snapshot 'export') -- normalized! +CREATE_REPLICATION_SLOT "slot_Name1" PHYSICAL (snapshot '_') -- literals removed + +parse +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY PHYSICAL EXPORT_SNAPSHOT TWO_PHASE +---- +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY PHYSICAL (snapshot 'export', two_phase 'true') -- normalized! +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY PHYSICAL (snapshot '_', two_phase '_') -- literals removed + +parse +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY PHYSICAL (a b, c 'd', e 1234, "F") +---- +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY PHYSICAL (a 'b', c 'd', e 1234, "F") -- normalized! +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY PHYSICAL (a '_', c '_', e _, "F") -- literals removed + +parse error +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL +---- +at or near "LOGICAL": syntax error +DETAIL: source SQL: +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL + ^ + +parse +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL wal2json +---- +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL wal2json +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL wal2json -- literals removed + +parse +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL wal2json EXPORT_SNAPSHOT +---- +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL wal2json (snapshot 'export') -- normalized! +CREATE_REPLICATION_SLOT "slot_Name1" LOGICAL wal2json (snapshot '_') -- literals removed + +parse +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY LOGICAL wal2json EXPORT_SNAPSHOT TWO_PHASE +---- +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY LOGICAL wal2json (snapshot 'export', two_phase 'true') -- normalized! +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY LOGICAL wal2json (snapshot '_', two_phase '_') -- literals removed + +parse +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY LOGICAL wal2json (a b, c 'd', e 1234, "F") +---- +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY LOGICAL wal2json (a 'b', c 'd', e 1234, "F") -- normalized! +CREATE_REPLICATION_SLOT "slot_Name1" TEMPORARY LOGICAL wal2json (a '_', c '_', e _, "F") -- literals removed diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/parser/drop_replication_slot.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/parser/drop_replication_slot.ddt new file mode 100644 index 000000000000..ef16da413d96 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/parser/drop_replication_slot.ddt @@ -0,0 +1,11 @@ +parse +DROP_REPLICATION_SLOT "A2s_x" +---- +DROP_REPLICATION_SLOT "A2s_x" +DROP_REPLICATION_SLOT "A2s_x" -- literals removed + +parse +DROP_REPLICATION_SLOT "A2s_x" WAIT +---- +DROP_REPLICATION_SLOT "A2s_x" WAIT +DROP_REPLICATION_SLOT "A2s_x" WAIT -- literals removed diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/parser/identify_system.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/parser/identify_system.ddt new file mode 100644 index 000000000000..9fa65fe86dec --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/parser/identify_system.ddt @@ -0,0 +1,19 @@ +parse +IDENTIFY_SYSTEM +---- +IDENTIFY_SYSTEM +IDENTIFY_SYSTEM -- literals removed + +parse +IDENTIFY_SYSTEM; +---- +IDENTIFY_SYSTEM -- normalized! +IDENTIFY_SYSTEM -- literals removed + +parse error +iDENTIFY_SYSTEM +---- +at or near "identify_system": syntax error +DETAIL: source SQL: +iDENTIFY_SYSTEM +^ diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/parser/read_replication_slot.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/parser/read_replication_slot.ddt new file mode 100644 index 000000000000..f4e640b782e7 --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/parser/read_replication_slot.ddt @@ -0,0 +1,5 @@ +parse +READ_REPLICATION_SLOT "a" +---- +READ_REPLICATION_SLOT a -- normalized! +READ_REPLICATION_SLOT a -- literals removed diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/parser/start_replication.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/parser/start_replication.ddt new file mode 100644 index 000000000000..9c189f4ea12b --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/parser/start_replication.ddt @@ -0,0 +1,79 @@ +parse +START_REPLICATION A0010/1011 +---- +START_REPLICATION PHYSICAL A0010/1011 -- normalized! +START_REPLICATION PHYSICAL A0010/1011 -- literals removed + +parse +START_REPLICATION PHYSICAL A0010/1011 +---- +START_REPLICATION PHYSICAL A0010/1011 +START_REPLICATION PHYSICAL A0010/1011 -- literals removed + +parse +START_REPLICATION SLOT slot_1 A0010/1011 TIMELINE 3 +---- +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE 3 -- normalized! +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE _ -- literals removed + +parse +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE 3 +---- +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE 3 +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE _ -- literals removed + +parse error +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE 0 +---- +expected a positive integer for timeline + +parse error +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE 3354731204578932752358923785325325 +---- +at or near "3354731204578932752358923785325325": syntax error +DETAIL: source SQL: +START_REPLICATION SLOT slot_1 PHYSICAL A0010/1011 TIMELINE 3354731204578932752358923785325325 + ^ + +parse error +START_REPLICATION SLOT slot_1 A0010/1011G +---- +at or near "g": syntax error +DETAIL: source SQL: +START_REPLICATION SLOT slot_1 A0010/1011G + ^ + +parse +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 +---- +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 -- literals removed + +parse +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 (OPT 'opt') +---- +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 (opt 'opt') -- normalized! +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 (opt '_') -- literals removed + +parse +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 (OPT 'opt', "xXx" 'a') +---- +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 (opt 'opt', "xXx" 'a') -- normalized! +START_REPLICATION SLOT slot_1 LOGICAL A0010/1011 (opt '_', "xXx" '_') -- literals removed + +# bad LSN +parse error +START_REPLICATION SLOT slot_1 LOGICAL G0010/101 +---- +at or near "g0010": syntax error +DETAIL: source SQL: +START_REPLICATION SLOT slot_1 LOGICAL G0010/101 + ^ + +parse error +START_REPLICATION SLOT slot_1 LOGICAL F0010/ +---- +at or near "f0010": syntax error +DETAIL: source SQL: +START_REPLICATION SLOT slot_1 LOGICAL F0010/ + ^ diff --git a/pkg/sql/pgrepl/pgreplparser/testdata/parser/timeline_history.ddt b/pkg/sql/pgrepl/pgreplparser/testdata/parser/timeline_history.ddt new file mode 100644 index 000000000000..9cad288ef59e --- /dev/null +++ b/pkg/sql/pgrepl/pgreplparser/testdata/parser/timeline_history.ddt @@ -0,0 +1,32 @@ +parse error +TIMELINE_HISTORY +---- +at or near "TIMELINE_HISTORY": syntax error +DETAIL: source SQL: +TIMELINE_HISTORY +^ + +parse +TIMELINE_HISTORY 1; +---- +TIMELINE_HISTORY 1 -- normalized! +TIMELINE_HISTORY _ -- literals removed + +parse +TIMELINE_HISTORY 1330 +---- +TIMELINE_HISTORY 1330 +TIMELINE_HISTORY _ -- literals removed + +parse error +TIMELINE_HISTORY 0 +---- +expected a positive integer for timeline + +parse error +TIMELINE_HISTORY 133032475823895789234759378957238957328943289745381974981238947 +---- +at or near "133032475823895789234759378957238957328943289745381974981238947": syntax error +DETAIL: source SQL: +TIMELINE_HISTORY 133032475823895789234759378957238957328943289745381974981238947 + ^ diff --git a/pkg/sql/pgrepl/pgrepltree/BUILD.bazel b/pkg/sql/pgrepl/pgrepltree/BUILD.bazel new file mode 100644 index 000000000000..b0dd10bdcd82 --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "pgrepltree", + srcs = [ + "base_backup.go", + "create_replication_slot.go", + "drop_replication_slot.go", + "identify_system.go", + "option.go", + "pgrepltree.go", + "read_replication_slot.go", + "replication_slot.go", + "start_replication.go", + "timeline_history.go", + ], + importpath = "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/pgrepltree", + visibility = ["//visibility:public"], + deps = [ + "//pkg/sql/pgrepl/lsn", + "//pkg/sql/sem/tree", + ], +) diff --git a/pkg/sql/pgrepl/pgrepltree/base_backup.go b/pkg/sql/pgrepl/pgrepltree/base_backup.go new file mode 100644 index 000000000000..d4a2bd0451fe --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/base_backup.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +type BaseBackup struct { + Options Options +} + +var _ tree.Statement = (*BaseBackup)(nil) + +func (bb *BaseBackup) String() string { + return tree.AsString(bb) +} + +func (bb *BaseBackup) Format(ctx *tree.FmtCtx) { + ctx.WriteString("BASE_BACKUP") + if len(bb.Options) > 0 { + ctx.WriteString(" (") + ctx.FormatNode(bb.Options) + ctx.WriteString(")") + } +} + +func (bb *BaseBackup) StatementReturnType() tree.StatementReturnType { + return tree.Replication +} + +func (bb *BaseBackup) StatementType() tree.StatementType { + return tree.TypeDDL +} + +func (bb *BaseBackup) StatementTag() string { + return "BASE_BACKUP" +} + +func (bb *BaseBackup) replicationStatement() { +} + +var _ tree.Statement = (*BaseBackup)(nil) diff --git a/pkg/sql/pgrepl/pgrepltree/create_replication_slot.go b/pkg/sql/pgrepl/pgrepltree/create_replication_slot.go new file mode 100644 index 000000000000..036e6423da4f --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/create_replication_slot.go @@ -0,0 +1,70 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import ( + "fmt" + + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" +) + +type CreateReplicationSlot struct { + Slot tree.Name + Temporary bool + Kind SlotKind + Plugin tree.Name + Options Options +} + +var _ tree.Statement = (*CreateReplicationSlot)(nil) + +func (crs *CreateReplicationSlot) String() string { + return tree.AsString(crs) +} + +func (crs *CreateReplicationSlot) Format(ctx *tree.FmtCtx) { + ctx.WriteString("CREATE_REPLICATION_SLOT ") + ctx.FormatNode(&crs.Slot) + if crs.Temporary { + ctx.WriteString(" TEMPORARY") + } + switch crs.Kind { + case PhysicalReplication: + ctx.WriteString(" PHYSICAL") + case LogicalReplication: + ctx.WriteString(" LOGICAL ") + ctx.FormatNode(&crs.Plugin) + default: + panic(fmt.Sprintf("unknown replication slot kind: %d", crs.Kind)) + } + if len(crs.Options) > 0 { + ctx.WriteString(" (") + ctx.FormatNode(crs.Options) + ctx.WriteString(")") + } +} + +func (crs *CreateReplicationSlot) StatementReturnType() tree.StatementReturnType { + return tree.Replication +} + +func (crs *CreateReplicationSlot) StatementType() tree.StatementType { + return tree.TypeDDL +} + +func (crs *CreateReplicationSlot) StatementTag() string { + return "CREATE_REPLICATION_SLOT" +} + +func (crs *CreateReplicationSlot) replicationStatement() { +} + +var _ tree.Statement = (*CreateReplicationSlot)(nil) diff --git a/pkg/sql/pgrepl/pgrepltree/drop_replication_slot.go b/pkg/sql/pgrepl/pgrepltree/drop_replication_slot.go new file mode 100644 index 000000000000..9d9d81be056d --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/drop_replication_slot.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +type DropReplicationSlot struct { + Slot tree.Name + Wait bool +} + +var _ tree.Statement = (*DropReplicationSlot)(nil) + +func (drs *DropReplicationSlot) String() string { + return tree.AsString(drs) +} + +func (drs *DropReplicationSlot) Format(ctx *tree.FmtCtx) { + ctx.WriteString("DROP_REPLICATION_SLOT ") + ctx.FormatNode(&drs.Slot) + if drs.Wait { + ctx.WriteString(" WAIT") + } +} + +func (drs *DropReplicationSlot) StatementReturnType() tree.StatementReturnType { + return tree.Replication +} + +func (drs *DropReplicationSlot) StatementType() tree.StatementType { + return tree.TypeDDL +} + +func (drs *DropReplicationSlot) StatementTag() string { + return "DROP_REPLICATION_SLOT" +} + +func (drs *DropReplicationSlot) replicationStatement() { +} + +var _ tree.Statement = (*DropReplicationSlot)(nil) diff --git a/pkg/sql/pgrepl/pgrepltree/identify_system.go b/pkg/sql/pgrepl/pgrepltree/identify_system.go new file mode 100644 index 000000000000..e0573e5dff8a --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/identify_system.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +type IdentifySystem struct { +} + +var _ tree.Statement = (*IdentifySystem)(nil) + +func (i *IdentifySystem) String() string { + return tree.AsString(i) +} + +func (i *IdentifySystem) Format(ctx *tree.FmtCtx) { + ctx.WriteString("IDENTIFY_SYSTEM") +} + +func (i *IdentifySystem) StatementReturnType() tree.StatementReturnType { + return tree.Replication +} + +func (i *IdentifySystem) StatementType() tree.StatementType { + return tree.TypeDDL +} + +func (i *IdentifySystem) StatementTag() string { + return "IDENTIFY_SYSTEM" +} + +func (i *IdentifySystem) replicationStatement() { +} + +var _ tree.Statement = (*IdentifySystem)(nil) diff --git a/pkg/sql/pgrepl/pgrepltree/option.go b/pkg/sql/pgrepl/pgrepltree/option.go new file mode 100644 index 000000000000..ae8fb58cc067 --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/option.go @@ -0,0 +1,37 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +type Options []Option + +func (ol Options) Format(ctx *tree.FmtCtx) { + for i, o := range ol { + if i > 0 { + ctx.WriteString(", ") + } + ctx.FormatNode(o) + } +} + +type Option struct { + Key tree.Name + Value tree.Expr +} + +func (o Option) Format(ctx *tree.FmtCtx) { + ctx.FormatNode(&o.Key) + if o.Value != nil { + ctx.WriteRune(' ') + ctx.FormatNode(o.Value) + } +} diff --git a/pkg/sql/pgrepl/pgrepltree/pgrepltree.go b/pkg/sql/pgrepl/pgrepltree/pgrepltree.go new file mode 100644 index 000000000000..f30ab72f0888 --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/pgrepltree.go @@ -0,0 +1,19 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +// Package pgrepltree contains the AST structs for pg replication types. +package pgrepltree + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +type ReplicationStatement interface { + tree.Statement + replicationStatement() +} diff --git a/pkg/sql/pgrepl/pgrepltree/read_replication_slot.go b/pkg/sql/pgrepl/pgrepltree/read_replication_slot.go new file mode 100644 index 000000000000..d7aca552de82 --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/read_replication_slot.go @@ -0,0 +1,45 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +type ReadReplicationSlot struct { + Slot tree.Name +} + +var _ tree.Statement = (*ReadReplicationSlot)(nil) + +func (rrs *ReadReplicationSlot) String() string { + return tree.AsString(rrs) +} + +func (rrs *ReadReplicationSlot) Format(ctx *tree.FmtCtx) { + ctx.WriteString("READ_REPLICATION_SLOT ") + ctx.FormatNode(&rrs.Slot) +} + +func (rrs *ReadReplicationSlot) StatementReturnType() tree.StatementReturnType { + return tree.Replication +} + +func (rrs *ReadReplicationSlot) StatementType() tree.StatementType { + return tree.TypeDDL +} + +func (rrs *ReadReplicationSlot) StatementTag() string { + return "READ_REPLICATION_SLOT" +} + +func (rrs *ReadReplicationSlot) replicationStatement() { +} + +var _ tree.Statement = (*ReadReplicationSlot)(nil) diff --git a/pkg/sql/pgrepl/pgrepltree/replication_slot.go b/pkg/sql/pgrepl/pgrepltree/replication_slot.go new file mode 100644 index 000000000000..56fd02f33cf8 --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/replication_slot.go @@ -0,0 +1,18 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +type SlotKind int + +const ( + PhysicalReplication SlotKind = iota + 1 + LogicalReplication +) diff --git a/pkg/sql/pgrepl/pgrepltree/start_replication.go b/pkg/sql/pgrepl/pgrepltree/start_replication.go new file mode 100644 index 000000000000..13cc9ffddab2 --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/start_replication.go @@ -0,0 +1,76 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import ( + "fmt" + + "github.com/cockroachdb/cockroach/pkg/sql/pgrepl/lsn" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" +) + +type StartReplication struct { + Slot tree.Name + Temporary bool + Kind SlotKind + LSN lsn.LSN + Options Options + Timeline *tree.NumVal +} + +var _ tree.Statement = (*StartReplication)(nil) + +func (srs *StartReplication) String() string { + return tree.AsString(srs) +} + +func (srs *StartReplication) Format(ctx *tree.FmtCtx) { + ctx.WriteString("START_REPLICATION") + if srs.Slot != "" { + ctx.WriteString(" SLOT ") + ctx.FormatNode(&srs.Slot) + } + switch srs.Kind { + case PhysicalReplication: + ctx.WriteString(" PHYSICAL ") + case LogicalReplication: + ctx.WriteString(" LOGICAL ") + default: + panic(fmt.Sprintf("unknown replication slot kind: %d", srs.Kind)) + } + ctx.WriteString(srs.LSN.String()) + if srs.Timeline != nil { + ctx.WriteString(" TIMELINE ") + ctx.FormatNode(srs.Timeline) + } + if len(srs.Options) > 0 { + ctx.WriteString(" (") + ctx.FormatNode(srs.Options) + ctx.WriteString(")") + } +} + +func (srs *StartReplication) StatementReturnType() tree.StatementReturnType { + return tree.Replication +} + +func (srs *StartReplication) StatementType() tree.StatementType { + return tree.TypeDDL +} + +func (srs *StartReplication) StatementTag() string { + return "START_REPLICATION" +} + +func (srs *StartReplication) replicationStatement() { +} + +var _ tree.Statement = (*StartReplication)(nil) diff --git a/pkg/sql/pgrepl/pgrepltree/timeline_history.go b/pkg/sql/pgrepl/pgrepltree/timeline_history.go new file mode 100644 index 000000000000..b9ba5a7329fe --- /dev/null +++ b/pkg/sql/pgrepl/pgrepltree/timeline_history.go @@ -0,0 +1,45 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgrepltree + +import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" + +type TimelineHistory struct { + Timeline *tree.NumVal +} + +var _ tree.Statement = (*TimelineHistory)(nil) + +func (th *TimelineHistory) String() string { + return tree.AsString(th) +} + +func (th *TimelineHistory) Format(ctx *tree.FmtCtx) { + ctx.WriteString("TIMELINE_HISTORY ") + ctx.FormatNode(th.Timeline) +} + +func (th *TimelineHistory) StatementReturnType() tree.StatementReturnType { + return tree.Replication +} + +func (th *TimelineHistory) StatementType() tree.StatementType { + return tree.TypeDDL +} + +func (th *TimelineHistory) StatementTag() string { + return "TIMELINE_HISTORY" +} + +func (th *TimelineHistory) replicationStatement() { +} + +var _ tree.Statement = (*TimelineHistory)(nil) diff --git a/pkg/sql/sem/tree/statementreturntype_string.go b/pkg/sql/sem/tree/statementreturntype_string.go index 2a1bec9b0361..e0be27df3520 100644 --- a/pkg/sql/sem/tree/statementreturntype_string.go +++ b/pkg/sql/sem/tree/statementreturntype_string.go @@ -14,7 +14,8 @@ func _() { _ = x[Rows-3] _ = x[CopyIn-4] _ = x[CopyOut-5] - _ = x[Unknown-6] + _ = x[Replication-6] + _ = x[Unknown-7] } func (i StatementReturnType) String() string { @@ -31,6 +32,8 @@ func (i StatementReturnType) String() string { return "CopyIn" case CopyOut: return "CopyOut" + case Replication: + return "Replication" case Unknown: return "Unknown" default: diff --git a/pkg/sql/sem/tree/stmt.go b/pkg/sql/sem/tree/stmt.go index 686ccd1d1b0c..db475c56aa0a 100644 --- a/pkg/sql/sem/tree/stmt.go +++ b/pkg/sql/sem/tree/stmt.go @@ -63,6 +63,8 @@ const ( CopyIn // CopyOut indicates a COPY TO statement. CopyOut + // Replication indicates a replication protocol statement. + Replication // Unknown indicates that the statement does not have a known // return style at the time of parsing. This is not first in the // enumeration because it is more convenient to have Ack as a zero