Skip to content

Commit 104edf2

Browse files
authored
Merge pull request #18 from sqlcj-dev/feature/postgresql-table-level-constraint-parsing
feat(schema): accept table-level foreign key and check constraints
2 parents fdb07c3 + f4e33ad commit 104edf2

3 files changed

Lines changed: 240 additions & 13 deletions

File tree

‎src/main/java/dev/sqlcj/schema/parser/DefaultSchemaParser.java‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import net.sf.jsqlparser.schema.MultiPartName;
1212
import net.sf.jsqlparser.statement.Statement;
1313
import net.sf.jsqlparser.statement.Statements;
14+
import net.sf.jsqlparser.statement.create.table.CheckConstraint;
1415
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
1516
import net.sf.jsqlparser.statement.create.table.CreateTable;
17+
import net.sf.jsqlparser.statement.create.table.ForeignKeyIndex;
1618
import net.sf.jsqlparser.statement.create.table.Index;
1719

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

194196
for (Index index : indexes) {
195-
String indexType = index.getType();
196-
197-
ConstraintType type = switch (indexType.toUpperCase(Locale.ROOT)) {
198-
case "PRIMARY KEY" -> ConstraintType.PRIMARY_KEY;
199-
case "UNIQUE" -> ConstraintType.UNIQUE;
200-
default -> throw new UnsupportedOperationException(
201-
"Unsupported table constraint: " + indexType
202-
);
203-
};
197+
if (isIgnoredTableConstraint(index)) {
198+
continue;
199+
}
204200

205201
constraints.add(
206202
new Constraint(
207-
type,
203+
constraintType(index),
208204
index.getColumnsNames().stream()
209205
.map(MultiPartName::unquote)
210206
.toList()
@@ -214,4 +210,28 @@ private List<Constraint> parseTableConstraints(CreateTable createTable) {
214210

215211
return constraints;
216212
}
213+
214+
/**
215+
* Foreign key and check constraints are accepted but not modeled, because
216+
* they do not affect the generated Java types.
217+
*/
218+
private boolean isIgnoredTableConstraint(Index index) {
219+
return index instanceof ForeignKeyIndex || index instanceof CheckConstraint;
220+
}
221+
222+
private ConstraintType constraintType(Index index) {
223+
String indexType = index.getType();
224+
225+
String declaredType = indexType == null
226+
? ""
227+
: indexType.toUpperCase(Locale.ROOT);
228+
229+
return switch (declaredType) {
230+
case "PRIMARY KEY" -> ConstraintType.PRIMARY_KEY;
231+
case "UNIQUE" -> ConstraintType.UNIQUE;
232+
default -> throw new UnsupportedOperationException(
233+
"Unsupported table constraint: " + indexType
234+
);
235+
};
236+
}
217237
}

‎src/test/java/dev/sqlcj/compiler/PostgresIntegrationTest.java‎

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,35 @@ updated_at TIMESTAMP(3) WITH TIME ZONE
7474
);
7575
""";
7676

77+
/**
78+
* A snapshot whose table-level foreign key and check constraints are
79+
* accepted and ignored by the schema parser, together with the column
80+
* defaults and named constraints that surround them.
81+
*/
82+
private static final String CONSTRAINT_SCHEMA = """
83+
CREATE TABLE customers
84+
(
85+
id BIGINT NOT NULL,
86+
name VARCHAR(255) NOT NULL,
87+
CONSTRAINT customers_pk PRIMARY KEY (id),
88+
CONSTRAINT customers_name_unique UNIQUE (name)
89+
);
90+
91+
CREATE TABLE customer_orders
92+
(
93+
id BIGINT NOT NULL,
94+
customer_id BIGINT NOT NULL,
95+
quantity INTEGER NOT NULL DEFAULT 1,
96+
status VARCHAR(32) DEFAULT 'new',
97+
created_at TIMESTAMP DEFAULT now(),
98+
PRIMARY KEY (id),
99+
CONSTRAINT customer_orders_customer_fk FOREIGN KEY (customer_id)
100+
REFERENCES customers (id) ON DELETE CASCADE,
101+
CONSTRAINT customer_orders_quantity_check CHECK (quantity > 0),
102+
CHECK (status <> '')
103+
);
104+
""";
105+
77106
private static final UUID EXTERNAL_ID = UUID.fromString("3f2504e0-4f89-11d3-9a0c-0305e82c3301");
78107

79108
private static final OffsetDateTime UPDATED_AT = OffsetDateTime.of(
@@ -128,6 +157,8 @@ static void stopPostgres() {
128157
*/
129158
@BeforeEach
130159
void resetDatabase() throws Exception {
160+
execute("DROP TABLE IF EXISTS customer_orders");
161+
execute("DROP TABLE IF EXISTS customers");
131162
execute("DROP TABLE IF EXISTS users");
132163
execute(SCHEMA);
133164
}
@@ -453,16 +484,64 @@ INSERT INTO users (id, code, serial_id, revision, external_id, updated_at)
453484
}
454485

455486
/**
456-
* Compiles the schema snapshot and the given queries, then compiles every
457-
* generated Java file into an isolated temporary classes directory.
487+
* Proves that a snapshot carrying table-level foreign key and check
488+
* constraints, column defaults, and named constraints is valid PostgreSQL
489+
* DDL, compiles through the pipeline, and reads back the values the
490+
* database defaults produced.
458491
*/
492+
@Test
493+
void shouldExecuteGeneratedQueryForSnapshotWithIgnoredTableConstraints() throws Exception {
494+
execute(CONSTRAINT_SCHEMA);
495+
496+
Path classesDirectory = generateAndCompile(
497+
CONSTRAINT_SCHEMA,
498+
"""
499+
-- name: GetCustomerOrder :one
500+
SELECT id, customer_id, quantity, status
501+
FROM customer_orders
502+
WHERE id = $1;
503+
"""
504+
);
505+
506+
execute("INSERT INTO customers (id, name) VALUES (1, 'Alice')");
507+
execute("INSERT INTO customer_orders (id, customer_id) VALUES (10, 1)");
508+
509+
try (URLClassLoader classLoader = classLoader(classesDirectory)) {
510+
Object query = newQuery(classLoader, "GetCustomerOrder");
511+
512+
Method method = query.getClass().getMethod("getCustomerOrder", Long.class);
513+
514+
Object result = method.invoke(query, 10L);
515+
516+
assertNotNull(result);
517+
518+
assertEquals(
519+
List.of("id", "customer_id", "quantity", "status"),
520+
recordComponentNames(result)
521+
);
522+
523+
assertEquals(10L, component(result, "id"));
524+
assertEquals(1L, component(result, "customer_id"));
525+
assertEquals(1, component(result, "quantity"));
526+
assertEquals("new", component(result, "status"));
527+
}
528+
}
529+
459530
private Path generateAndCompile(String queries) throws Exception {
531+
return generateAndCompile(SCHEMA, queries);
532+
}
533+
534+
/**
535+
* Compiles the given schema snapshot and queries, then compiles every
536+
* generated Java file into an isolated temporary classes directory.
537+
*/
538+
private Path generateAndCompile(String schema, String queries) throws Exception {
460539
Path schemaFile = tempDir.resolve("schema.sql");
461540
Path queriesFile = tempDir.resolve("queries.sql");
462541
Path generatedDirectory = tempDir.resolve("generated");
463542
Path classesDirectory = tempDir.resolve("classes");
464543

465-
Files.writeString(schemaFile, SCHEMA);
544+
Files.writeString(schemaFile, schema);
466545
Files.writeString(queriesFile, queries);
467546

468547
Config config = new Config(

‎src/test/java/dev/sqlcj/schema/parser/DefaultSchemaParserTest.java‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,134 @@ CREATE TABLE users (
291291
);
292292
}
293293

294+
@Test
295+
void shouldParseColumnSpecificationsThatDoNotAffectTypes() {
296+
String sql = """
297+
CREATE TABLE orders (
298+
id BIGINT NOT NULL PRIMARY KEY,
299+
quantity INTEGER NOT NULL DEFAULT 0 CHECK (quantity > 0),
300+
status VARCHAR(32) DEFAULT 'new',
301+
created_at TIMESTAMP DEFAULT now(),
302+
user_id BIGINT REFERENCES users (id),
303+
owner_id BIGINT REFERENCES users (id) ON DELETE CASCADE
304+
);
305+
""";
306+
307+
Schema schema = parser.parse(sql);
308+
309+
Table table = schema.tables().getFirst();
310+
311+
assertEquals(
312+
List.of(
313+
new Column("id", ColumnType.BIGINT, false),
314+
new Column("quantity", ColumnType.INTEGER, false),
315+
new Column("status", ColumnType.VARCHAR, true),
316+
new Column("created_at", ColumnType.TIMESTAMP, true),
317+
new Column("user_id", ColumnType.BIGINT, true),
318+
new Column("owner_id", ColumnType.BIGINT, true)
319+
),
320+
table.columns()
321+
);
322+
323+
assertEquals(
324+
List.of(
325+
new Constraint(
326+
ConstraintType.PRIMARY_KEY,
327+
List.of("id")
328+
)
329+
),
330+
table.constraints()
331+
);
332+
}
333+
334+
@ParameterizedTest
335+
@ValueSource(
336+
strings = {
337+
"FOREIGN KEY (user_id) REFERENCES users (id)",
338+
"CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE",
339+
"CHECK (quantity > 0)",
340+
"CONSTRAINT orders_quantity_check CHECK (quantity > 0)"
341+
}
342+
)
343+
void shouldIgnoreForeignKeyAndCheckTableConstraints(String tableConstraint) {
344+
String sql = """
345+
CREATE TABLE orders (
346+
id BIGINT NOT NULL,
347+
user_id BIGINT NOT NULL,
348+
quantity INTEGER NOT NULL,
349+
%s
350+
);
351+
"""
352+
.formatted(tableConstraint);
353+
354+
Schema schema = parser.parse(sql);
355+
356+
Table table = schema.tables().getFirst();
357+
358+
assertEquals(
359+
List.of(
360+
new Column("id", ColumnType.BIGINT, false),
361+
new Column("user_id", ColumnType.BIGINT, false),
362+
new Column("quantity", ColumnType.INTEGER, false)
363+
),
364+
table.columns()
365+
);
366+
367+
assertTrue(table.constraints().isEmpty());
368+
}
369+
370+
@Test
371+
void shouldParseNamedTableConstraintsAlongsideIgnoredOnes() {
372+
String sql = """
373+
CREATE TABLE orders (
374+
id BIGINT NOT NULL,
375+
code VARCHAR(32) NOT NULL,
376+
user_id BIGINT NOT NULL,
377+
quantity INTEGER NOT NULL DEFAULT 1,
378+
CONSTRAINT orders_pk PRIMARY KEY (id),
379+
CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users (id),
380+
CONSTRAINT orders_quantity_check CHECK (quantity > 0),
381+
CONSTRAINT orders_code_unique UNIQUE (code),
382+
FOREIGN KEY (code) REFERENCES codes (code),
383+
CHECK (quantity < 100)
384+
);
385+
""";
386+
387+
Schema schema = parser.parse(sql);
388+
389+
Table table = schema.tables().getFirst();
390+
391+
assertEquals(
392+
List.of(
393+
new Constraint(
394+
ConstraintType.PRIMARY_KEY,
395+
List.of("id")
396+
),
397+
new Constraint(
398+
ConstraintType.UNIQUE,
399+
List.of("code")
400+
)
401+
),
402+
table.constraints()
403+
);
404+
}
405+
406+
@Test
407+
void shouldRejectUnsupportedSchemaStatement() {
408+
String sql = """
409+
CREATE TABLE users (
410+
id BIGINT NOT NULL
411+
);
412+
413+
ALTER TABLE users ADD COLUMN name VARCHAR(255);
414+
""";
415+
416+
assertThrows(
417+
UnsupportedOperationException.class,
418+
() -> parser.parse(sql)
419+
);
420+
}
421+
294422
@Test
295423
void shouldThrowSchemaParseExceptionForInvalidSql() {
296424
String sql = """

0 commit comments

Comments
 (0)