From e2f02b6a09548e74304cbc6e32b6ce720d6dba84 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Fri, 16 Oct 2026 09:11:52 +0100 Subject: Add primary key constraint to SQL AST --- hobgoblin/src/output/lang/sql/ast.hob | 15 ++++++ .../compiler/output/lang/sql/SqlWriter.java | 35 +++++++++++-- .../lang/sql/ast/SqlCreateTableStatement.java | 36 ++++++++++---- .../lang/sql/ast/SqlPrimaryKeyConstraint.java | 44 ++++++++++++++++ .../output/lang/sql/ast/SqlTableConstraint.java | 18 +++++++ .../compiler/output/lang/sql/SqlWriterTests.java | 58 ++++++++++++++++++++++ .../sql/ast/SqlCreateTableStatementMatcher.java | 6 +++ .../sql/ast/SqlPrimaryKeyConstraintMatcher.java | 41 +++++++++++++++ 8 files changed, 240 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraint.java create mode 100644 src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlTableConstraint.java create mode 100644 src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraintMatcher.java diff --git a/hobgoblin/src/output/lang/sql/ast.hob b/hobgoblin/src/output/lang/sql/ast.hob index 9a51e38..f5dad7d 100644 --- a/hobgoblin/src/output/lang/sql/ast.hob +++ b/hobgoblin/src/output/lang/sql/ast.hob @@ -1,3 +1,5 @@ +// == Statements == + sum SqlStatement { variant SqlCreateTableStatement; } @@ -5,6 +7,7 @@ sum SqlStatement { struct SqlCreateTableStatement { field name: SqlIdentifier; field columns: List[SqlColumnDefinition]; + field tableConstraints: List[SqlTableConstraint]; } struct SqlColumnDefinition { @@ -12,6 +15,18 @@ struct SqlColumnDefinition { field typeName: SqlIdentifier; } +// == Table constraints == + +sum SqlTableConstraint { + variant SqlPrimaryKeyConstraint; +} + +struct SqlPrimaryKeyConstraint { + field columns: List[SqlIdentifier]; +} + +// == Identifiers == + struct SqlIdentifier { field value: String; } diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriter.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriter.java index 20b28c3..b7d0caf 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriter.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriter.java @@ -1,10 +1,7 @@ package org.zwobble.hobgoblin.compiler.output.lang.sql; import org.zwobble.hobgoblin.compiler.output.CodeWriter; -import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlColumnDefinition; -import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement; -import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier; -import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlStatement; +import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.*; import java.io.IOException; import java.nio.file.Path; @@ -40,6 +37,8 @@ public class SqlWriter { this.writer = writer; } + // == Statements == + void writeStatement(SqlStatement statement) throws IOException { switch (statement) { case SqlCreateTableStatement createTableStatement -> { @@ -64,6 +63,12 @@ public class SqlWriter { } ); + for (var tableConstraint : createTableStatement.tableConstraints()) { + this.writer.write(","); + this.writer.newLine(); + writeTableConstraint(tableConstraint); + } + this.writer.dedent(); this.writer.newLine(); this.writer.write(");"); @@ -75,6 +80,28 @@ public class SqlWriter { this.writeIdentifier(column.typeName()); } + // == Table constraints == + + void writeTableConstraint(SqlTableConstraint tableConstraint) throws IOException { + switch (tableConstraint) { + case SqlPrimaryKeyConstraint primaryKeyConstraint -> { + writePrimaryKeyConstraint(primaryKeyConstraint); + } + } + } + + private void writePrimaryKeyConstraint(SqlPrimaryKeyConstraint primaryKeyConstraint) throws IOException { + this.writer.write("PRIMARY KEY ("); + writeWithSeparator( + primaryKeyConstraint.columns(), + this::writeIdentifier, + () -> this.writer.write(", ") + ); + this.writer.write(")"); + } + + // == Identifiers == + private void writeIdentifier(SqlIdentifier name) throws IOException { // TODO: handle escaping this.writer.write(name.value()); diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatement.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatement.java index a5fec23..85594a2 100644 --- a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatement.java +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatement.java @@ -7,42 +7,60 @@ package org.zwobble.hobgoblin.compiler.output.lang.sql.ast; public record SqlCreateTableStatement( org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier name, - java.util.List columns + java.util.List columns, + java.util.List tableConstraints ) implements org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlStatement { public static org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder arbitrary() { - return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier.arbitrary().build(), java.util.List.of()); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier.arbitrary().build(), java.util.List.of(), java.util.List.of()); } public record Builder( org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier name, - java.util.List columns + java.util.List columns, + java.util.List tableConstraints ) implements org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlStatement.Builder { public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement build() { - return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement(name, columns); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement(name, columns, tableConstraints); } public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder withName(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier name) { - return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns, tableConstraints); } public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder withName(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier.Builder name) { - return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name.build(), columns); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name.build(), columns, tableConstraints); } public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder withColumns(java.util.List columns) { - return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns, tableConstraints); } public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder addColumn(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlColumnDefinition column) { var columns = new java.util.ArrayList(this.columns); columns.add(column); - return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns, tableConstraints); } public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder addColumn(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlColumnDefinition.Builder column) { var columns = new java.util.ArrayList(this.columns); columns.add(column.build()); - return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns, tableConstraints); + } + + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder withTableConstraints(java.util.List tableConstraints) { + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns, tableConstraints); + } + + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder addTableConstraint(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint tableConstraint) { + var tableConstraints = new java.util.ArrayList(this.tableConstraints); + tableConstraints.add(tableConstraint); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns, tableConstraints); + } + + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder addTableConstraint(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint.Builder tableConstraint) { + var tableConstraints = new java.util.ArrayList(this.tableConstraints); + tableConstraints.add(tableConstraint.build()); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder(name, columns, tableConstraints); } // Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement.Builder body diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraint.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraint.java new file mode 100644 index 0000000..1876219 --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraint.java @@ -0,0 +1,44 @@ +// Generated by hobgoblin. + +package org.zwobble.hobgoblin.compiler.output.lang.sql.ast; + +// Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint imports +// Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint imports + +public record SqlPrimaryKeyConstraint( + java.util.List columns +) implements org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint { + public static org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder arbitrary() { + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder(java.util.List.of()); + } + + public record Builder( + java.util.List columns + ) implements org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint.Builder { + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint build() { + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint(columns); + } + + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder withColumns(java.util.List columns) { + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder(columns); + } + + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder addColumn(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier column) { + var columns = new java.util.ArrayList(this.columns); + columns.add(column); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder(columns); + } + + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder addColumn(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier.Builder column) { + var columns = new java.util.ArrayList(this.columns); + columns.add(column.build()); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder(columns); + } + + // Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder body + // Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.Builder body + } + + // Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint body + // Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint body +} diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlTableConstraint.java b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlTableConstraint.java new file mode 100644 index 0000000..e35162f --- /dev/null +++ b/src/main/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlTableConstraint.java @@ -0,0 +1,18 @@ +// Generated by hobgoblin. + +package org.zwobble.hobgoblin.compiler.output.lang.sql.ast; + +// Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint imports +// Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint imports + +public sealed interface SqlTableConstraint permits org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint { + public interface Builder { + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint build(); + + // Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint.Builder body + // Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint.Builder body + } + + // Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint body + // Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlTableConstraint body +} diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriterTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriterTests.java index 5edf06c..2d8c7d3 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriterTests.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/SqlWriterTests.java @@ -5,6 +5,7 @@ import org.zwobble.hobgoblin.compiler.output.CodeWriter; import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlColumnDefinition; import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement; import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlIdentifier; +import org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint; import java.io.IOException; import java.io.StringWriter; @@ -14,6 +15,8 @@ import static org.zwobble.precisely.AssertThat.assertThat; import static org.zwobble.precisely.Matchers.equalTo; public class SqlWriterTests { + // == Create table statements == + @Test public void createTableStatement() throws IOException { var sql = SqlCreateTableStatement.arbitrary() @@ -38,6 +41,61 @@ public class SqlWriterTests { )); } + @Test + public void createTableStatementWithTableConstraint() throws IOException { + var sql = SqlCreateTableStatement.arbitrary() + .withName(id("authors")) + .addColumn(SqlColumnDefinition.arbitrary() + .withName(id("id")) + .withTypeName(id("INT")) + ) + .addColumn(SqlColumnDefinition.arbitrary() + .withName(id("name")) + .withTypeName(id("TEXT")) + ) + .addTableConstraint(SqlPrimaryKeyConstraint.arbitrary() + .addColumn(id("id")) + ) + .build(); + + var string = write(writer -> writer.writeStatement(sql)); + + assertThat(string, equalTo(""" + CREATE TABLE authors ( + id INT, + name TEXT, + PRIMARY KEY (id) + );""" + )); + } + + // == Table constraints == + + @Test + public void primaryKeyConstraintWithSingleColumn() throws IOException { + var sql = SqlPrimaryKeyConstraint.arbitrary() + .addColumn(id("id")) + .build(); + + var string = write(writer -> writer.writeTableConstraint(sql)); + + assertThat(string, equalTo("PRIMARY KEY (id)")); + } + + @Test + public void primaryKeyConstraintWithMultipleColumns() throws IOException { + var sql = SqlPrimaryKeyConstraint.arbitrary() + .addColumn(id("a")) + .addColumn(id("b")) + .build(); + + var string = write(writer -> writer.writeTableConstraint(sql)); + + assertThat(string, equalTo("PRIMARY KEY (a, b)")); + } + + // == Test utilities == + private String write(SqlWriter.Write write) throws IOException { return write(write, Optional.empty()); } diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatementMatcher.java b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatementMatcher.java index d6c8d57..d5024ef 100644 --- a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatementMatcher.java +++ b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlCreateTableStatementMatcher.java @@ -42,6 +42,12 @@ public final class SqlCreateTableStatementMatcher implements org.zwobble.precise return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatementMatcher(submatchers); } + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatementMatcher withTableConstraints(org.zwobble.precisely.Matcher> tableConstraints) { + var submatchers = new java.util.ArrayList>(this.submatchers); + submatchers.add(org.zwobble.precisely.Matchers.has("tableConstraints", org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatement::tableConstraints, (org.zwobble.precisely.Matcher) tableConstraints)); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatementMatcher(submatchers); + } + // Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatementMatcher body // Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlCreateTableStatementMatcher body } diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraintMatcher.java b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraintMatcher.java new file mode 100644 index 0000000..500896b --- /dev/null +++ b/src/test/java/org/zwobble/hobgoblin/compiler/output/lang/sql/ast/SqlPrimaryKeyConstraintMatcher.java @@ -0,0 +1,41 @@ +// Generated by hobgoblin. + +package org.zwobble.hobgoblin.compiler.output.lang.sql.ast; + +// Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher imports +// Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher imports + +public final class SqlPrimaryKeyConstraintMatcher implements org.zwobble.precisely.Matcher { + public static org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher isSqlPrimaryKeyConstraint() { + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher(java.util.List.of()); + } + + private final java.util.List> submatchers; + + private SqlPrimaryKeyConstraintMatcher( + java.util.List> submatchers + ) { + this.submatchers = submatchers; + } + + public org.zwobble.precisely.MatchResult match(java.lang.Object actual) { + return this.toMatcher().match(actual); + } + + public org.zwobble.precisely.TextTree describe() { + return this.toMatcher().describe(); + } + + private org.zwobble.precisely.Matcher toMatcher() { + return org.zwobble.precisely.Matchers.instanceOf(org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint.class, this.submatchers); + } + + public org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher withColumns(org.zwobble.precisely.Matcher> columns) { + var submatchers = new java.util.ArrayList>(this.submatchers); + submatchers.add(org.zwobble.precisely.Matchers.has("columns", org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraint::columns, (org.zwobble.precisely.Matcher) columns)); + return new org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher(submatchers); + } + + // Custom area start: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher body + // Custom area end: org.zwobble.hobgoblin.compiler.output.lang.sql.ast.SqlPrimaryKeyConstraintMatcher body +} -- cgit v1.2.3