summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2026-10-16 08:44:55 +0100
committerMichael Williamson <mike@zwobble.org>2026-10-16 08:44:55 +0100
commit77c708cbb7a4fb49db50a8917bdcdd777eff6ccb (patch)
tree4aeb55e2ff372c980e3893302a0f0166f656d67c
parent8fe3ffe8c7aad4c3270b889bce0efaf4ab912157 (diff)
Check primary key field exists
-rw-r--r--hobgoblin/src/ast/typed.hob3
-rw-r--r--hobgoblin/src/types.hob3
-rw-r--r--src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNode.java8
-rw-r--r--src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java16
-rw-r--r--src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/NoSuchFieldError.java24
-rw-r--r--src/main/java/org/zwobble/hobgoblin/compiler/types/HobgoblinNativeTypes.java6
-rw-r--r--src/test/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNodeMatcher.java2
-rw-r--r--src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerDatabaseSchemaDefinitionTests.java9
-rw-r--r--src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTableDefinitionTests.java41
9 files changed, 99 insertions, 13 deletions
diff --git a/hobgoblin/src/ast/typed.hob b/hobgoblin/src/ast/typed.hob
index b65ecdd..a734d7c 100644
--- a/hobgoblin/src/ast/typed.hob
+++ b/hobgoblin/src/ast/typed.hob
@@ -2,6 +2,7 @@ import {DocComment} from ast;
import {Source} from sources;
import {
EnumType,
+ Field,
NamespaceName,
SimpleNativeType,
SimpleStructType,
@@ -89,7 +90,7 @@ sum TypedDatabaseSchemaStatementNode {
struct TypedTableDefinitionNode {
field name: String;
field rowType: StructType;
- field primaryKey: String;
+ field primaryKey: Field;
field docComment: DocComment;
field source: Source;
}
diff --git a/hobgoblin/src/types.hob b/hobgoblin/src/types.hob
index 5e96164..2473b6c 100644
--- a/hobgoblin/src/types.hob
+++ b/hobgoblin/src/types.hob
@@ -21,3 +21,6 @@ native SumType {
native TypeOrConstructor[T] {
}
+
+native Field {
+}
diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNode.java b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNode.java
index 315aeef..9a9f98b 100644
--- a/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNode.java
+++ b/src/main/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNode.java
@@ -8,18 +8,18 @@ package org.zwobble.hobgoblin.compiler.ast.typed;
public record TypedTableDefinitionNode(
String name,
org.zwobble.hobgoblin.compiler.types.StructType rowType,
- String primaryKey,
+ org.zwobble.hobgoblin.compiler.types.Field primaryKey,
org.zwobble.hobgoblin.compiler.ast.DocComment docComment,
org.zwobble.hobgoblin.compiler.sources.Source source
) implements org.zwobble.hobgoblin.compiler.ast.typed.TypedDatabaseSchemaStatementNode {
public static org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode.Builder arbitrary() {
- return new org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode.Builder("", org.zwobble.hobgoblin.compiler.types.HobgoblinNativeTypes.arbitraryStructType(), "", org.zwobble.hobgoblin.compiler.ast.HobgoblinNativeAst.arbitraryDocComment(), org.zwobble.hobgoblin.compiler.sources.HobgoblinNativeSources.arbitrarySource());
+ return new org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode.Builder("", org.zwobble.hobgoblin.compiler.types.HobgoblinNativeTypes.arbitraryStructType(), org.zwobble.hobgoblin.compiler.types.HobgoblinNativeTypes.arbitraryField(), org.zwobble.hobgoblin.compiler.ast.HobgoblinNativeAst.arbitraryDocComment(), org.zwobble.hobgoblin.compiler.sources.HobgoblinNativeSources.arbitrarySource());
}
public record Builder(
String name,
org.zwobble.hobgoblin.compiler.types.StructType rowType,
- String primaryKey,
+ org.zwobble.hobgoblin.compiler.types.Field primaryKey,
org.zwobble.hobgoblin.compiler.ast.DocComment docComment,
org.zwobble.hobgoblin.compiler.sources.Source source
) implements org.zwobble.hobgoblin.compiler.ast.typed.TypedDatabaseSchemaStatementNode.Builder {
@@ -35,7 +35,7 @@ public record TypedTableDefinitionNode(
return new org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode.Builder(name, rowType, primaryKey, docComment, source);
}
- public org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode.Builder withPrimaryKey(String primaryKey) {
+ public org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode.Builder withPrimaryKey(org.zwobble.hobgoblin.compiler.types.Field primaryKey) {
return new org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode.Builder(name, rowType, primaryKey, docComment, source);
}
diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java
index 211726b..c8dfc05 100644
--- a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java
+++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/TypeChecker.java
@@ -5,6 +5,7 @@ import org.zwobble.hobgoblin.compiler.ast.untyped.*;
import org.zwobble.hobgoblin.compiler.builtins.NativeTypes;
import org.zwobble.hobgoblin.compiler.sources.Source;
import org.zwobble.hobgoblin.compiler.typechecker.errors.*;
+import org.zwobble.hobgoblin.compiler.typechecker.errors.NoSuchFieldError;
import org.zwobble.hobgoblin.compiler.types.*;
import java.util.ArrayList;
@@ -470,18 +471,27 @@ public class TypeChecker {
) {
var rowTypeTypedNode = typeCheckMetaType(untyped.rowType(), context);
- if (!(rowTypeTypedNode.value() instanceof StructType rowType)) {
+ // TODO: allow constructed struct types
+ if (!(rowTypeTypedNode.value() instanceof SimpleStructType rowType)) {
throw new TableRowTypeMustBeStructError(
rowTypeTypedNode.value(),
rowTypeTypedNode.source()
);
}
+ var fields = context.fieldsOf(rowType).orElse(List.of());
+ var primaryKeyField = fields.stream()
+ .filter(field -> field.name().equals(untyped.primaryKey()))
+ .findFirst();
+ if (primaryKeyField.isEmpty()) {
+ // TODO: more precise source.
+ throw new NoSuchFieldError(rowType, untyped.primaryKey(), untyped.source());
+ }
+
return new TypedTableDefinitionNode(
untyped.name(),
rowType,
- // TODO: check primary key is valid.
- untyped.primaryKey(),
+ primaryKeyField.get(),
untyped.docComment(),
untyped.source()
);
diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/NoSuchFieldError.java b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/NoSuchFieldError.java
new file mode 100644
index 0000000..635ea6f
--- /dev/null
+++ b/src/main/java/org/zwobble/hobgoblin/compiler/typechecker/errors/NoSuchFieldError.java
@@ -0,0 +1,24 @@
+package org.zwobble.hobgoblin.compiler.typechecker.errors;
+
+import org.zwobble.hobgoblin.compiler.errors.SourceError;
+import org.zwobble.hobgoblin.compiler.sources.Source;
+import org.zwobble.hobgoblin.compiler.types.Type;
+
+public class NoSuchFieldError extends SourceError {
+ private final Type type;
+ private final String fieldName;
+
+ public NoSuchFieldError(Type type, String fieldName, Source source) {
+ super(type.describe() + " does not have a field named " + fieldName, source);
+ this.type = type;
+ this.fieldName = fieldName;
+ }
+
+ public Type type() {
+ return type;
+ }
+
+ public String fieldName() {
+ return fieldName;
+ }
+}
diff --git a/src/main/java/org/zwobble/hobgoblin/compiler/types/HobgoblinNativeTypes.java b/src/main/java/org/zwobble/hobgoblin/compiler/types/HobgoblinNativeTypes.java
index 37e6935..96efbd2 100644
--- a/src/main/java/org/zwobble/hobgoblin/compiler/types/HobgoblinNativeTypes.java
+++ b/src/main/java/org/zwobble/hobgoblin/compiler/types/HobgoblinNativeTypes.java
@@ -1,5 +1,7 @@
package org.zwobble.hobgoblin.compiler.types;
+import org.zwobble.hobgoblin.compiler.sources.NullSource;
+
import java.util.function.Supplier;
public class HobgoblinNativeTypes {
@@ -34,4 +36,8 @@ public class HobgoblinNativeTypes {
public static <T extends ConstructibleType> TypeOrConstructor<T> arbitraryTypeOrConstructor(Supplier<T> type) {
return TypeOrConstructor.type(type.get());
}
+
+ public static Field arbitraryField() {
+ return new Field("", arbitraryType(), NullSource.INSTANCE);
+ }
}
diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNodeMatcher.java b/src/test/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNodeMatcher.java
index 18b827b..64554c9 100644
--- a/src/test/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNodeMatcher.java
+++ b/src/test/java/org/zwobble/hobgoblin/compiler/ast/typed/TypedTableDefinitionNodeMatcher.java
@@ -42,7 +42,7 @@ public final class TypedTableDefinitionNodeMatcher implements org.zwobble.precis
return new org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNodeMatcher(submatchers);
}
- public org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNodeMatcher withPrimaryKey(org.zwobble.precisely.Matcher<? super String> primaryKey) {
+ public org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNodeMatcher withPrimaryKey(org.zwobble.precisely.Matcher<? super org.zwobble.hobgoblin.compiler.types.Field> primaryKey) {
var submatchers = new java.util.ArrayList<org.zwobble.precisely.Matcher<? super org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode>>(this.submatchers);
submatchers.add(org.zwobble.precisely.Matchers.has("primaryKey", org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNode::primaryKey, (org.zwobble.precisely.Matcher) primaryKey));
return new org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNodeMatcher(submatchers);
diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerDatabaseSchemaDefinitionTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerDatabaseSchemaDefinitionTests.java
index 574bf26..08fdd47 100644
--- a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerDatabaseSchemaDefinitionTests.java
+++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerDatabaseSchemaDefinitionTests.java
@@ -4,11 +4,16 @@ import org.junit.jupiter.api.Test;
import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedDatabaseSchemaDefinitionNode;
import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedTableDefinitionNode;
import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedTypeLevelReferenceNode;
+import org.zwobble.hobgoblin.compiler.builtins.NativeTypes;
import org.zwobble.hobgoblin.compiler.sources.NullSource;
+import org.zwobble.hobgoblin.compiler.types.Field;
import org.zwobble.hobgoblin.compiler.types.NamespaceName;
import org.zwobble.hobgoblin.compiler.types.SimpleStructType;
import org.zwobble.hobgoblin.compiler.types.Types;
+import java.util.List;
+import java.util.Optional;
+
import static org.zwobble.hobgoblin.compiler.ast.typed.TypedDatabaseSchemaDefinitionNodeMatcher.isTypedDatabaseSchemaDefinitionNode;
import static org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNodeMatcher.isTypedTableDefinitionNode;
import static org.zwobble.hobgoblin.compiler.typechecker.TypeCheckerTesting.typeCheckNamespaceStatement;
@@ -27,6 +32,7 @@ public class TypeCheckerDatabaseSchemaDefinitionTests {
.withName("Author")
.build()
)
+ .withPrimaryKey("id")
.build()
)
.build();
@@ -34,6 +40,9 @@ public class TypeCheckerDatabaseSchemaDefinitionTests {
var namespaceContext = globalContext.enterNamespace(NamespaceName.of("App"));
var authorType = new SimpleStructType(NamespaceName.of("App"), "Author");
namespaceContext.declare("Author", Types.metaType(authorType), NullSource.INSTANCE);
+ namespaceContext.defineStructType(authorType, Optional.of(List.of(
+ new Field("id", NativeTypes.INT_32, NullSource.INSTANCE)
+ )));
var typed = typeCheckNamespaceStatement(untyped, namespaceContext);
diff --git a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTableDefinitionTests.java b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTableDefinitionTests.java
index a4e1cf4..a32945a 100644
--- a/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTableDefinitionTests.java
+++ b/src/test/java/org/zwobble/hobgoblin/compiler/typechecker/TypeCheckerTableDefinitionTests.java
@@ -3,12 +3,14 @@ package org.zwobble.hobgoblin.compiler.typechecker;
import org.junit.jupiter.api.Test;
import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedTableDefinitionNode;
import org.zwobble.hobgoblin.compiler.ast.untyped.UntypedTypeLevelReferenceNode;
+import org.zwobble.hobgoblin.compiler.builtins.NativeTypes;
import org.zwobble.hobgoblin.compiler.sources.NullSource;
+import org.zwobble.hobgoblin.compiler.typechecker.errors.NoSuchFieldError;
import org.zwobble.hobgoblin.compiler.typechecker.errors.TableRowTypeMustBeStructError;
-import org.zwobble.hobgoblin.compiler.types.NamespaceName;
-import org.zwobble.hobgoblin.compiler.types.SimpleNativeType;
-import org.zwobble.hobgoblin.compiler.types.SimpleStructType;
-import org.zwobble.hobgoblin.compiler.types.Types;
+import org.zwobble.hobgoblin.compiler.types.*;
+
+import java.util.List;
+import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.zwobble.hobgoblin.compiler.ast.typed.TypedTableDefinitionNodeMatcher.isTypedTableDefinitionNode;
@@ -25,11 +27,15 @@ public class TypeCheckerTableDefinitionTests {
.withName("Author")
.build()
)
+ .withPrimaryKey("id")
.build();
var globalContext = TypeCheckerContextArb.globalContext();
var namespaceContext = globalContext.enterNamespace(NamespaceName.of("App"));
var authorType = new SimpleStructType(NamespaceName.of("App"), "Author");
namespaceContext.declare("Author", Types.metaType(authorType), NullSource.INSTANCE);
+ namespaceContext.defineStructType(authorType, Optional.of(List.of(
+ new Field("id", NativeTypes.INT_32, NullSource.INSTANCE)
+ )));
var typed = typeCheckDatabaseSchemaStatement(untyped, namespaceContext);
@@ -60,4 +66,31 @@ public class TypeCheckerTableDefinitionTests {
assertThat(error.rowType(), equalTo(authorType));
}
+
+ @Test
+ public void whenPrimaryKeyIsNotStructFieldThenErrorIsThrown() {
+ var untyped = UntypedTableDefinitionNode.arbitrary()
+ .withName("authors")
+ .withRowType(UntypedTypeLevelReferenceNode.arbitrary()
+ .withName("Author")
+ .build()
+ )
+ .withPrimaryKey("id")
+ .build();
+ var globalContext = TypeCheckerContextArb.globalContext();
+ var namespaceContext = globalContext.enterNamespace(NamespaceName.of("App"));
+ var authorType = new SimpleStructType(NamespaceName.of("App"), "Author");
+ namespaceContext.declare("Author", Types.metaType(authorType), NullSource.INSTANCE);
+ namespaceContext.defineStructType(authorType, Optional.of(List.of(
+ new Field("author_id", NativeTypes.INT_32, NullSource.INSTANCE)
+ )));
+
+ var error = assertThrows(
+ NoSuchFieldError.class,
+ () -> typeCheckDatabaseSchemaStatement(untyped, namespaceContext)
+ );
+
+ assertThat(error.type(), equalTo(authorType));
+ assertThat(error.fieldName(), equalTo("id"));
+ }
}