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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions src/main/java/dev/sqlcj/schema/parser/DefaultSchemaParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import net.sf.jsqlparser.schema.MultiPartName;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.create.table.CheckConstraint;
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex;
import net.sf.jsqlparser.statement.create.table.Index;

import java.util.ArrayList;
Expand Down Expand Up @@ -192,19 +194,13 @@ private List<Constraint> parseTableConstraints(CreateTable createTable) {
List<Constraint> constraints = new ArrayList<>();

for (Index index : indexes) {
String indexType = index.getType();

ConstraintType type = switch (indexType.toUpperCase(Locale.ROOT)) {
case "PRIMARY KEY" -> ConstraintType.PRIMARY_KEY;
case "UNIQUE" -> ConstraintType.UNIQUE;
default -> throw new UnsupportedOperationException(
"Unsupported table constraint: " + indexType
);
};
if (isIgnoredTableConstraint(index)) {
continue;
}

constraints.add(
new Constraint(
type,
constraintType(index),
index.getColumnsNames().stream()
.map(MultiPartName::unquote)
.toList()
Expand All @@ -214,4 +210,28 @@ private List<Constraint> parseTableConstraints(CreateTable createTable) {

return constraints;
}

/**
* Foreign key and check constraints are accepted but not modeled, because
* they do not affect the generated Java types.
*/
private boolean isIgnoredTableConstraint(Index index) {
return index instanceof ForeignKeyIndex || index instanceof CheckConstraint;
}

private ConstraintType constraintType(Index index) {
String indexType = index.getType();

String declaredType = indexType == null
? ""
: indexType.toUpperCase(Locale.ROOT);

return switch (declaredType) {
case "PRIMARY KEY" -> ConstraintType.PRIMARY_KEY;
case "UNIQUE" -> ConstraintType.UNIQUE;
default -> throw new UnsupportedOperationException(
"Unsupported table constraint: " + indexType
);
};
}
}
85 changes: 82 additions & 3 deletions src/test/java/dev/sqlcj/compiler/PostgresIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@ updated_at TIMESTAMP(3) WITH TIME ZONE
);
""";

/**
* A snapshot whose table-level foreign key and check constraints are
* accepted and ignored by the schema parser, together with the column
* defaults and named constraints that surround them.
*/
private static final String CONSTRAINT_SCHEMA = """
CREATE TABLE customers
(
id BIGINT NOT NULL,
name VARCHAR(255) NOT NULL,
CONSTRAINT customers_pk PRIMARY KEY (id),
CONSTRAINT customers_name_unique UNIQUE (name)
);

CREATE TABLE customer_orders
(
id BIGINT NOT NULL,
customer_id BIGINT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
status VARCHAR(32) DEFAULT 'new',
created_at TIMESTAMP DEFAULT now(),
PRIMARY KEY (id),
CONSTRAINT customer_orders_customer_fk FOREIGN KEY (customer_id)
REFERENCES customers (id) ON DELETE CASCADE,
CONSTRAINT customer_orders_quantity_check CHECK (quantity > 0),
CHECK (status <> '')
);
""";

private static final UUID EXTERNAL_ID = UUID.fromString("3f2504e0-4f89-11d3-9a0c-0305e82c3301");

private static final OffsetDateTime UPDATED_AT = OffsetDateTime.of(
Expand Down Expand Up @@ -128,6 +157,8 @@ static void stopPostgres() {
*/
@BeforeEach
void resetDatabase() throws Exception {
execute("DROP TABLE IF EXISTS customer_orders");
execute("DROP TABLE IF EXISTS customers");
execute("DROP TABLE IF EXISTS users");
execute(SCHEMA);
}
Expand Down Expand Up @@ -453,16 +484,64 @@ INSERT INTO users (id, code, serial_id, revision, external_id, updated_at)
}

/**
* Compiles the schema snapshot and the given queries, then compiles every
* generated Java file into an isolated temporary classes directory.
* Proves that a snapshot carrying table-level foreign key and check
* constraints, column defaults, and named constraints is valid PostgreSQL
* DDL, compiles through the pipeline, and reads back the values the
* database defaults produced.
*/
@Test
void shouldExecuteGeneratedQueryForSnapshotWithIgnoredTableConstraints() throws Exception {
execute(CONSTRAINT_SCHEMA);

Path classesDirectory = generateAndCompile(
CONSTRAINT_SCHEMA,
"""
-- name: GetCustomerOrder :one
SELECT id, customer_id, quantity, status
FROM customer_orders
WHERE id = $1;
"""
);

execute("INSERT INTO customers (id, name) VALUES (1, 'Alice')");
execute("INSERT INTO customer_orders (id, customer_id) VALUES (10, 1)");

try (URLClassLoader classLoader = classLoader(classesDirectory)) {
Object query = newQuery(classLoader, "GetCustomerOrder");

Method method = query.getClass().getMethod("getCustomerOrder", Long.class);

Object result = method.invoke(query, 10L);

assertNotNull(result);

assertEquals(
List.of("id", "customer_id", "quantity", "status"),
recordComponentNames(result)
);

assertEquals(10L, component(result, "id"));
assertEquals(1L, component(result, "customer_id"));
assertEquals(1, component(result, "quantity"));
assertEquals("new", component(result, "status"));
}
}

private Path generateAndCompile(String queries) throws Exception {
return generateAndCompile(SCHEMA, queries);
}

/**
* Compiles the given schema snapshot and queries, then compiles every
* generated Java file into an isolated temporary classes directory.
*/
private Path generateAndCompile(String schema, String queries) throws Exception {
Path schemaFile = tempDir.resolve("schema.sql");
Path queriesFile = tempDir.resolve("queries.sql");
Path generatedDirectory = tempDir.resolve("generated");
Path classesDirectory = tempDir.resolve("classes");

Files.writeString(schemaFile, SCHEMA);
Files.writeString(schemaFile, schema);
Files.writeString(queriesFile, queries);

Config config = new Config(
Expand Down
128 changes: 128 additions & 0 deletions src/test/java/dev/sqlcj/schema/parser/DefaultSchemaParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,134 @@ CREATE TABLE users (
);
}

@Test
void shouldParseColumnSpecificationsThatDoNotAffectTypes() {
String sql = """
CREATE TABLE orders (
id BIGINT NOT NULL PRIMARY KEY,
quantity INTEGER NOT NULL DEFAULT 0 CHECK (quantity > 0),
status VARCHAR(32) DEFAULT 'new',
created_at TIMESTAMP DEFAULT now(),
user_id BIGINT REFERENCES users (id),
owner_id BIGINT REFERENCES users (id) ON DELETE CASCADE
);
""";

Schema schema = parser.parse(sql);

Table table = schema.tables().getFirst();

assertEquals(
List.of(
new Column("id", ColumnType.BIGINT, false),
new Column("quantity", ColumnType.INTEGER, false),
new Column("status", ColumnType.VARCHAR, true),
new Column("created_at", ColumnType.TIMESTAMP, true),
new Column("user_id", ColumnType.BIGINT, true),
new Column("owner_id", ColumnType.BIGINT, true)
),
table.columns()
);

assertEquals(
List.of(
new Constraint(
ConstraintType.PRIMARY_KEY,
List.of("id")
)
),
table.constraints()
);
}

@ParameterizedTest
@ValueSource(
strings = {
"FOREIGN KEY (user_id) REFERENCES users (id)",
"CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE",
"CHECK (quantity > 0)",
"CONSTRAINT orders_quantity_check CHECK (quantity > 0)"
}
)
void shouldIgnoreForeignKeyAndCheckTableConstraints(String tableConstraint) {
String sql = """
CREATE TABLE orders (
id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
quantity INTEGER NOT NULL,
%s
);
"""
.formatted(tableConstraint);

Schema schema = parser.parse(sql);

Table table = schema.tables().getFirst();

assertEquals(
List.of(
new Column("id", ColumnType.BIGINT, false),
new Column("user_id", ColumnType.BIGINT, false),
new Column("quantity", ColumnType.INTEGER, false)
),
table.columns()
);

assertTrue(table.constraints().isEmpty());
}

@Test
void shouldParseNamedTableConstraintsAlongsideIgnoredOnes() {
String sql = """
CREATE TABLE orders (
id BIGINT NOT NULL,
code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
CONSTRAINT orders_pk PRIMARY KEY (id),
CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users (id),
CONSTRAINT orders_quantity_check CHECK (quantity > 0),
CONSTRAINT orders_code_unique UNIQUE (code),
FOREIGN KEY (code) REFERENCES codes (code),
CHECK (quantity < 100)
);
""";

Schema schema = parser.parse(sql);

Table table = schema.tables().getFirst();

assertEquals(
List.of(
new Constraint(
ConstraintType.PRIMARY_KEY,
List.of("id")
),
new Constraint(
ConstraintType.UNIQUE,
List.of("code")
)
),
table.constraints()
);
}

@Test
void shouldRejectUnsupportedSchemaStatement() {
String sql = """
CREATE TABLE users (
id BIGINT NOT NULL
);

ALTER TABLE users ADD COLUMN name VARCHAR(255);
""";

assertThrows(
UnsupportedOperationException.class,
() -> parser.parse(sql)
);
}

@Test
void shouldThrowSchemaParseExceptionForInvalidSql() {
String sql = """
Expand Down
Loading