Skip to content

Commit

Permalink
feat: support annotations in SQL file (#171)
Browse files Browse the repository at this point in the history
* feat: support annotations in SQL file

Column Annotations are NOT a feature of Cloud Spanner.
 
This is an additional feature of the Cloud Spanner Schema parser exclusively in this tool so that users of this tool can add metadata to columns, and have that metadata represented in the parsed schema.

See description in src/main/jjtree-sources/ddl_annotation.jjt

---------

Co-authored-by: Niel Markwick <[email protected]>
  • Loading branch information
gurminder71 and nielm authored Sep 27, 2024
1 parent b1b237b commit 63efbd6
Show file tree
Hide file tree
Showing 15 changed files with 384 additions and 5 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
src/main/jjtree-sources/ddl_keywords.jjt \
src/main/jjtree-sources/ddl_string_bytes_tokens.jjt \
src/main/jjtree-sources/ddl_expression.jjt \
src/main/jjtree-sources/ddl_annotation.jjt \
src/main/jjtree-sources/ddl_parser.jjt \
&gt; ${project.build.directory}/generated-sources/jjtree-src/DdlParser.jjt</argument>
</arguments>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -707,6 +708,27 @@ private static String getDatabaseNameFromAlterDatabase(List<ASTddl_statement> st
* @throws DdlDiffException if there is an error in parsing the DDL
*/
public static List<ASTddl_statement> parseDdl(String original) throws DdlDiffException {
return parseDdl(original, false);
}

/**
* Parses the Cloud Spanner Schema (DDL) string to a list of AST DDL statements.
*
* @param original DDL to parse
* @param parseAnnotationInComments If true then the annotations that appear as comments
* "-- @ANNOTATION annotation" will be parsed
* @return List of parsed DDL statements
*/
public static List<ASTddl_statement> parseDdl(String original, boolean parseAnnotationInComments)
throws DdlDiffException {
// the annotations are prefixed with "--" so that SQL file remains valid.
// strip the comment prefix before so that annotations can be parsed.
// otherwise they will be ignored as comment lines
if (parseAnnotationInComments) {
original =
Pattern.compile("^\\s*--\\s+@", Pattern.MULTILINE).matcher(original).replaceAll("@");
}

// Remove "--" comments and split by ";"
List<String> statements = Splitter.on(';').splitToList(original.replaceAll("--.*(\n|$)", ""));
ArrayList<ASTddl_statement> ddlStatements = new ArrayList<>(statements.size());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.solutions.spannerddl.parser;

import static com.google.cloud.solutions.spannerddl.diff.AstTreeUtils.getChildByType;

import com.google.cloud.solutions.spannerddl.diff.AstTreeUtils;
import java.util.ArrayList;
import java.util.List;

/**
* Abstract Syntax Tree parser object for "annotation" token
*
* <p>Column Annotations are NOT a feature of Cloud Spanner.
*
* <p>This is an additional feature of the Cloud Spanner Schema parser exclusively in this tool so
* that users of this tool can add metadata to colums, and have that metadata represented in the
* parsed schema.
*
* <p>To use Annotations, they should be added to a CREATE TABLE statement as follows:
*
* <pre>
* CREATE TABLE Albums (
* -- @ANNOTATION SOMETEXT,
* id STRING(36),
* ) PRIMARY KEY (id)
* </pre>
*
* Annotations need to be on their own line, and terminate with a comma. (This is because the '-- '
* prefix is removed before using the JJT parser).
*
* <p>As they are comments, they are ignored by the diff generator and by Spanner itself.
*/
public class ASTannotation extends SimpleNode {
public ASTannotation(int id) {
super(id);
}

public ASTannotation(DdlParser p, int id) {
super(p, id);
}

public String getName() {
return AstTreeUtils.tokensToString(getChildByType(children, ASTname.class));
}

public List<ASTannotation_param> getParams() {
List<ASTannotation_param> params = new ArrayList<>();
for (Node child : children) {
if (child instanceof ASTannotation_param) {
params.add((ASTannotation_param) child);
}
}
return params;
}

public String getAnnotation() {
return AstTreeUtils.tokensToString(this, false).replaceAll(" ", "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.solutions.spannerddl.parser;

import com.google.cloud.solutions.spannerddl.diff.AstTreeUtils;

public class ASTannotation_param extends SimpleNode {
public ASTannotation_param(int id) {
super(id);
}

public ASTannotation_param(DdlParser p, int id) {
super(p, id);
}

public String getKey() {
ASTparam_key key = AstTreeUtils.getChildByType(this, ASTparam_key.class);
return AstTreeUtils.tokensToString(key);
}

public String getValue() {
ASTparam_val val = AstTreeUtils.getOptionalChildByType(this, ASTparam_val.class);
if (val != null) {
return AstTreeUtils.tokensToString(val);
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ private void validateChildren() {
ASTcheck_constraint.class,
ASTprimary_key.class,
ASTtable_interleave_clause.class,
ASTrow_deletion_policy_clause.class));
ASTrow_deletion_policy_clause.class,
ASTannotation.class));
}

@Override
Expand Down
1 change: 1 addition & 0 deletions src/main/jjtree-sources/DdlParser.head
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ cat src/main/jjtree-sources/DdlParser.head \
src/main/jjtree-sources/ddl_keywords.jjt \
src/main/jjtree-sources/ddl_string_bytes_tokens.jjt \
src/main/jjtree-sources/ddl_expression.jjt \
src/main/jjtree-sources/ddl_annotation.jjt \
src/main/jjtree-sources/ddl_parser.jjt \
> src/main/jjtree/DdlParser.jjt

Expand Down
61 changes: 61 additions & 0 deletions src/main/jjtree-sources/ddl_annotation.jjt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

// Column Annotations are NOT a feature of Cloud Spanner.
//
// This is an additional feature of the Cloud Spanner Schema parser exclusively
// in this tool so that users of this tool can add metadata to colums, and have
// that metadata represented in the parsed schema.
//
// To use Annotations, they should be added to a CREATE TABLE statement as
// follows:
//
// CREATE TABLE Albums (
// -- @ANNOTATION SOMETEXT,
// id STRING(36),
// ) PRIMARY KEY (id)
//
// Annotations need to be on their own line, and terminate with a comma.
// (This is because the '-- ' prefix is removed before using the JJT parser).
//
// As they are comments, they are ignored by the diff generator and by
// Spanner itself.
//

TOKEN:
{
<ANNOTATION: "@ANNOTATION">
}

void column_annotation() #void: {}
{
<ANNOTATION> annotation()
}

void annotation(): {}
{
qualified_identifier() #name [ annotation_params() ]
}

void annotation_params() #void: {}
{
"(" annotation_param() ( "," annotation_param() )* ")"
}

void annotation_param(): {}
{
identifier() #param_key [ "=" identifier() #param_val ]
}
5 changes: 5 additions & 0 deletions src/main/jjtree-sources/ddl_parser.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ void table_element() #void :
LOOKAHEAD(3) foreign_key()
| LOOKAHEAD(3) check_constraint()
| LOOKAHEAD(3) synonym_clause()

// Column annotations are not a Spanner feature.
// See file ddl_annotation.jjt for more details.
| column_annotation()

| column_def()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.google.cloud.solutions.spannerddl.parser;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;

import com.google.cloud.solutions.spannerddl.diff.DdlDiff;
import com.google.cloud.solutions.spannerddl.diff.DdlDiffException;
import com.google.cloud.solutions.spannerddl.testUtils.ReadTestDatafile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.junit.Test;

public class DDLAnnotationTest {

@Test
public void validateAnnotations() throws IOException {
Map<String, String> tests = ReadTestDatafile.readDdlSegmentsFromFile("annotations.txt");
Map<String, String> expects =
ReadTestDatafile.readDdlSegmentsFromFile("expectedAnnotations.txt");

Iterator<Entry<String, String>> testIt = tests.entrySet().iterator();
Iterator<Entry<String, String>> expectedIt = expects.entrySet().iterator();

while (testIt.hasNext() && expectedIt.hasNext()) {
Entry<String, String> test = testIt.next();
String expected = expectedIt.next().getValue();
String segmentName = test.getKey();

try {
// first get all the annotations without removing the comment prefix
List<String> annotations = getTableAnnotations(test.getValue(), false);

// annotations should be empty
assertThat(annotations).isEmpty();

// now get all the annotations after removing the comment prefix
annotations = getTableAnnotations(test.getValue(), true);

List<String> expectedList =
expected != null ? Arrays.asList(expected.split("\n")) : Collections.emptyList();

assertWithMessage("Mismatch for section " + segmentName)
.that(annotations)
.isEqualTo(expectedList);
} catch (DdlDiffException e) {
fail("Failed to parse section: '" + segmentName + "': " + e);
}
}
}

private List<String> getTableAnnotations(String ddl, boolean parseAnnotations)
throws DdlDiffException {
List<String> annotations = new ArrayList<>();

List<ASTddl_statement> statements = DdlDiff.parseDdl(ddl, parseAnnotations);
for (ASTddl_statement statement : statements) {
if (statement.jjtGetChild(0).getId() == DdlParserTreeConstants.JJTCREATE_TABLE_STATEMENT) {
Node tableStatement = statement.jjtGetChild(0);
for (int i = 0, count = tableStatement.jjtGetNumChildren(); i < count; i++) {
Node child = tableStatement.jjtGetChild(i);
if (child instanceof ASTannotation) {
annotations.add(((ASTannotation) child).getAnnotation());
}
}
}
}
return annotations;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,26 @@ public abstract class ReadTestDatafile {
* @return LinkedHashMap of segment name => contents
*/
public static Map<String, String> readDdlSegmentsFromFile(String filename) throws IOException {
return readDdlSegmentsFromFile(filename, false);
}

/**
* Reads the test data file, parsing out the test titles and data from the file.
*
* @return LinkedHashMap of segment name => contents
*/
public static Map<String, String> readDdlSegmentsFromFile(String filename, boolean preserveSpace)
throws IOException {
File file = new File("src/test/resources/" + filename).getAbsoluteFile();
LinkedHashMap<String, String> output = new LinkedHashMap<>();

try (BufferedReader in = Files.newBufferedReader(file.toPath(), UTF_8)) {

String sectionName = null;
StringBuilder section = new StringBuilder();
String line;
while (null != (line = in.readLine())) {
line = line.replaceAll("#.*", "").trim();
String rawLine;
while (null != (rawLine = in.readLine())) {
String line = preserveSpace ? rawLine : rawLine.replaceAll("#.*", "").trim();
if (line.isEmpty()) {
continue;
}
Expand Down
12 changes: 12 additions & 0 deletions src/test/resources/annotations.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
== Test 1 all column annotations

CREATE TABLE test1 (
-- @ANNOTATION DEPRECATED,
-- @ANNOTATION PII,
-- @ANNOTATION TAG.business(internal),
-- @ANNOTATION TAG.business(key1,key2),
-- @ANNOTATION TAG.business(key1=val1,key2=value),
id STRING(36),
) PRIMARY KEY (id)

==
9 changes: 9 additions & 0 deletions src/test/resources/expectedAnnotations.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
== Test 1 all column annotations

DEPRECATED
PII
TAG.business(internal)
TAG.business(key1,key2)
TAG.business(key1=val1,key2=value)

==
Loading

0 comments on commit 63efbd6

Please sign in to comment.