Skip to content

Commit 723c2a4

Browse files
committed
feat(tests): add comprehensive tests for STRICT tables and constraint enforcement
1 parent dbe74eb commit 723c2a4

File tree

3 files changed

+936
-2
lines changed

3 files changed

+936
-2
lines changed

TODO.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ This document tracks the remaining tasks to complete the SQLite extraction from
5353
- [ ] Backup/restore operations
5454
-**Error handling tests**
5555
- ✅ SQL syntax errors
56-
- [ ] Constraint violations
56+
- ✅ Constraint violations - 10 comprehensive tests including CASCADE, deferred, CONFLICT clauses
57+
- ✅ STRICT tables - 17 comprehensive tests for type enforcement and constraints
5758
- [ ] Resource limits
5859
- [ ] Invalid operations
5960
-**Memory and performance tests**
@@ -281,7 +282,7 @@ This document tracks the remaining tasks to complete the SQLite extraction from
281282

282283
-**Core SQLite operations working** (CREATE, INSERT, SELECT, UPDATE, DELETE)
283284
-**Advanced SQLite features working** (user functions, aggregates, iterators, sessions, backup, and enhanced location method all fully functional)
284-
-**179 tests passing** with comprehensive coverage across all features:
285+
-**222 tests passing** with comprehensive coverage across all features:
285286
- ✅ 13 basic database tests
286287
- ✅ 13 configuration option tests
287288
- ✅ 8 user-defined function tests
@@ -295,6 +296,8 @@ This document tracks the remaining tasks to complete the SQLite extraction from
295296
- ✅ 28 SQLite session tests (with changeset content verification!)
296297
- ✅ 14 backup functionality tests (with Node.js API compatibility and rate validation)
297298
- ✅ 10 enhanced location method tests (with attached database support)
299+
- ✅ 26 error handling tests (with constraint violations and recovery)
300+
- ✅ 17 STRICT tables tests (with type enforcement and constraints)
298301
-**All core data types supported** (INTEGER, REAL, TEXT, BLOB, NULL, BigInt)
299302
-**Error handling working** for invalid SQL and operations
300303
-**Memory management working** with proper cleanup and N-API references

test/error-handling-simple.test.ts

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,358 @@ describe("Error Handling Tests - Safe Edition", () => {
135135

136136
db.close();
137137
});
138+
139+
test("handles AUTOINCREMENT constraint on non-INTEGER columns", () => {
140+
const db = new DatabaseSync(":memory:");
141+
142+
// AUTOINCREMENT can only be used with INTEGER PRIMARY KEY
143+
// Using it with TEXT should throw an error
144+
expect(() => {
145+
db.exec(`
146+
CREATE TABLE invalid_table (
147+
id TEXT PRIMARY KEY AUTOINCREMENT,
148+
name TEXT
149+
)
150+
`);
151+
}).toThrow(/AUTOINCREMENT.*only.*INTEGER PRIMARY KEY/i);
152+
153+
// AUTOINCREMENT on non-primary key should also fail
154+
expect(() => {
155+
db.exec(`
156+
CREATE TABLE invalid_table2 (
157+
id INTEGER AUTOINCREMENT,
158+
name TEXT
159+
)
160+
`);
161+
}).toThrow(/syntax error|AUTOINCREMENT/i);
162+
163+
// AUTOINCREMENT with composite primary key should fail
164+
expect(() => {
165+
db.exec(`
166+
CREATE TABLE invalid_table3 (
167+
id1 INTEGER,
168+
id2 INTEGER,
169+
name TEXT,
170+
PRIMARY KEY (id1, id2) AUTOINCREMENT
171+
)
172+
`);
173+
}).toThrow(/syntax error|near.*AUTOINCREMENT/i);
174+
175+
// Valid AUTOINCREMENT usage should work
176+
expect(() => {
177+
db.exec(`
178+
CREATE TABLE valid_table (
179+
id INTEGER PRIMARY KEY AUTOINCREMENT,
180+
name TEXT
181+
)
182+
`);
183+
}).not.toThrow();
184+
185+
db.close();
186+
});
187+
188+
test("handles DEFAULT constraint violations", () => {
189+
const db = new DatabaseSync(":memory:");
190+
191+
db.exec(`
192+
CREATE TABLE test_defaults (
193+
id INTEGER PRIMARY KEY,
194+
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
195+
created_at INTEGER DEFAULT (unixepoch()) NOT NULL
196+
)
197+
`);
198+
199+
// Valid insert with defaults
200+
const result = db
201+
.prepare("INSERT INTO test_defaults (id) VALUES (?)")
202+
.run(1);
203+
expect(result.changes).toBe(1);
204+
205+
// Verify defaults were applied
206+
const row = db.prepare("SELECT * FROM test_defaults WHERE id = 1").get();
207+
expect(row.status).toBe("active");
208+
expect(row.created_at).toBeGreaterThan(0);
209+
210+
// Explicit NULL should override DEFAULT but fail NOT NULL
211+
expect(() => {
212+
db.prepare(
213+
"INSERT INTO test_defaults (id, created_at) VALUES (?, ?)",
214+
).run(2, null);
215+
}).toThrow(/NOT NULL constraint failed/);
216+
217+
db.close();
218+
});
219+
220+
test("handles multiple constraint violations on same column", () => {
221+
const db = new DatabaseSync(":memory:");
222+
223+
db.exec(`
224+
CREATE TABLE complex_constraints (
225+
id INTEGER PRIMARY KEY,
226+
email TEXT UNIQUE NOT NULL CHECK (email LIKE '%@%'),
227+
age INTEGER NOT NULL CHECK (age >= 18 AND age <= 120)
228+
)
229+
`);
230+
231+
// Valid insert
232+
db.prepare(
233+
"INSERT INTO complex_constraints (email, age) VALUES (?, ?)",
234+
).run("user@example.com", 25);
235+
236+
// NULL email violates NOT NULL (checked before UNIQUE or CHECK)
237+
expect(() => {
238+
db.prepare(
239+
"INSERT INTO complex_constraints (email, age) VALUES (?, ?)",
240+
).run(null, 25);
241+
}).toThrow(/NOT NULL constraint failed/);
242+
243+
// Invalid email format violates CHECK
244+
expect(() => {
245+
db.prepare(
246+
"INSERT INTO complex_constraints (email, age) VALUES (?, ?)",
247+
).run("invalid-email", 25);
248+
}).toThrow(/CHECK constraint failed/);
249+
250+
// Duplicate email violates UNIQUE
251+
expect(() => {
252+
db.prepare(
253+
"INSERT INTO complex_constraints (email, age) VALUES (?, ?)",
254+
).run("user@example.com", 30);
255+
}).toThrow(/UNIQUE constraint failed/);
256+
257+
// Age out of range violates CHECK
258+
expect(() => {
259+
db.prepare(
260+
"INSERT INTO complex_constraints (email, age) VALUES (?, ?)",
261+
).run("another@example.com", 150);
262+
}).toThrow(/CHECK constraint failed/);
263+
264+
db.close();
265+
});
266+
267+
test("handles CASCADE constraint actions", () => {
268+
const db = new DatabaseSync(":memory:", {
269+
enableForeignKeyConstraints: true,
270+
});
271+
272+
db.exec(`
273+
CREATE TABLE authors (
274+
id INTEGER PRIMARY KEY,
275+
name TEXT NOT NULL
276+
);
277+
278+
CREATE TABLE books (
279+
id INTEGER PRIMARY KEY,
280+
title TEXT NOT NULL,
281+
author_id INTEGER NOT NULL,
282+
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE CASCADE
283+
);
284+
285+
CREATE TABLE reviews (
286+
id INTEGER PRIMARY KEY,
287+
book_id INTEGER NOT NULL,
288+
rating INTEGER NOT NULL,
289+
FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE RESTRICT
290+
);
291+
`);
292+
293+
// Insert test data
294+
db.prepare(
295+
"INSERT INTO authors (id, name) VALUES (1, 'Author One')",
296+
).run();
297+
db.prepare(
298+
"INSERT INTO books (id, title, author_id) VALUES (1, 'Book One', 1)",
299+
).run();
300+
db.prepare(
301+
"INSERT INTO reviews (id, book_id, rating) VALUES (1, 1, 5)",
302+
).run();
303+
304+
// CASCADE UPDATE should work
305+
const updateResult = db
306+
.prepare("UPDATE authors SET id = 2 WHERE id = 1")
307+
.run();
308+
expect(updateResult.changes).toBe(1);
309+
310+
// Verify book was updated
311+
const book = db.prepare("SELECT author_id FROM books WHERE id = 1").get();
312+
expect(book.author_id).toBe(2);
313+
314+
// RESTRICT DELETE should fail when referenced
315+
expect(() => {
316+
db.prepare("DELETE FROM books WHERE id = 1").run();
317+
}).toThrow(/FOREIGN KEY constraint failed/);
318+
319+
// Delete review first
320+
db.prepare("DELETE FROM reviews WHERE id = 1").run();
321+
322+
// CASCADE DELETE should work now
323+
const deleteResult = db.prepare("DELETE FROM authors WHERE id = 2").run();
324+
expect(deleteResult.changes).toBe(1);
325+
326+
// Verify book was deleted
327+
const bookCount = db.prepare("SELECT COUNT(*) as count FROM books").get();
328+
expect(bookCount.count).toBe(0);
329+
330+
db.close();
331+
});
332+
333+
test("handles deferred constraint checking", () => {
334+
const db = new DatabaseSync(":memory:", {
335+
enableForeignKeyConstraints: true,
336+
});
337+
338+
// CHECK constraints are always immediate in SQLite, not deferrable
339+
// But FOREIGN KEY constraints can be deferred
340+
db.exec(`
341+
CREATE TABLE parent (id INTEGER PRIMARY KEY);
342+
CREATE TABLE child (
343+
id INTEGER PRIMARY KEY,
344+
parent_id INTEGER,
345+
FOREIGN KEY (parent_id) REFERENCES parent(id) DEFERRABLE INITIALLY DEFERRED
346+
);
347+
`);
348+
349+
db.exec("BEGIN");
350+
// Insert child before parent (normally would fail with immediate constraint)
351+
db.prepare("INSERT INTO child (id, parent_id) VALUES (1, 1)").run();
352+
// Insert parent to satisfy constraint
353+
db.prepare("INSERT INTO parent (id) VALUES (1)").run();
354+
// Commit succeeds because constraint is satisfied at commit time
355+
expect(() => db.exec("COMMIT")).not.toThrow();
356+
357+
// Test that immediate foreign key constraints fail right away
358+
db.exec(`
359+
CREATE TABLE parent2 (id INTEGER PRIMARY KEY);
360+
CREATE TABLE child2 (
361+
id INTEGER PRIMARY KEY,
362+
parent_id INTEGER,
363+
FOREIGN KEY (parent_id) REFERENCES parent2(id) -- Not deferred
364+
);
365+
`);
366+
367+
db.exec("BEGIN");
368+
// This should fail immediately
369+
expect(() => {
370+
db.prepare("INSERT INTO child2 (id, parent_id) VALUES (1, 1)").run();
371+
}).toThrow(/FOREIGN KEY constraint failed/);
372+
db.exec("ROLLBACK");
373+
374+
db.close();
375+
});
376+
377+
test("handles CONFLICT clauses", () => {
378+
const db = new DatabaseSync(":memory:");
379+
380+
db.exec(`
381+
CREATE TABLE conflict_test (
382+
id INTEGER PRIMARY KEY,
383+
email TEXT UNIQUE ON CONFLICT IGNORE,
384+
username TEXT UNIQUE ON CONFLICT REPLACE,
385+
status TEXT DEFAULT 'active'
386+
)
387+
`);
388+
389+
// Insert initial data
390+
db.prepare(
391+
"INSERT INTO conflict_test (id, email, username) VALUES (1, 'user@example.com', 'user1')",
392+
).run();
393+
394+
// IGNORE conflict - insert is silently ignored
395+
const ignoreResult = db
396+
.prepare(
397+
"INSERT INTO conflict_test (id, email, username) VALUES (2, 'user@example.com', 'user2')",
398+
)
399+
.run();
400+
expect(ignoreResult.changes).toBe(0); // No rows inserted
401+
402+
// REPLACE conflict - existing row is replaced
403+
const replaceResult = db
404+
.prepare(
405+
"INSERT INTO conflict_test (id, email, username) VALUES (3, 'new@example.com', 'user1')",
406+
)
407+
.run();
408+
expect(replaceResult.changes).toBe(1);
409+
410+
// Verify the replacement happened
411+
const rows = db.prepare("SELECT * FROM conflict_test ORDER BY id").all();
412+
expect(rows.length).toBe(1); // Only one row remains
413+
expect(rows[0].id).toBe(3); // New id
414+
expect(rows[0].username).toBe("user1"); // Same username
415+
expect(rows[0].email).toBe("new@example.com"); // New email
416+
417+
db.close();
418+
});
419+
420+
test("handles partial indexes and constraints", () => {
421+
const db = new DatabaseSync(":memory:");
422+
423+
db.exec(`
424+
CREATE TABLE users (
425+
id INTEGER PRIMARY KEY,
426+
email TEXT,
427+
is_active INTEGER DEFAULT 1
428+
);
429+
430+
-- Unique constraint only for active users
431+
CREATE UNIQUE INDEX idx_active_email ON users(email) WHERE is_active = 1;
432+
`);
433+
434+
// Insert active user
435+
db.prepare(
436+
"INSERT INTO users (email, is_active) VALUES ('user@example.com', 1)",
437+
).run();
438+
439+
// Same email for inactive user is allowed
440+
const result = db
441+
.prepare(
442+
"INSERT INTO users (email, is_active) VALUES ('user@example.com', 0)",
443+
)
444+
.run();
445+
expect(result.changes).toBe(1);
446+
447+
// But duplicate active user email fails
448+
expect(() => {
449+
db.prepare(
450+
"INSERT INTO users (email, is_active) VALUES ('user@example.com', 1)",
451+
).run();
452+
}).toThrow(/UNIQUE constraint failed/);
453+
454+
db.close();
455+
});
456+
457+
test("handles generated columns constraints", () => {
458+
const db = new DatabaseSync(":memory:");
459+
460+
db.exec(`
461+
CREATE TABLE products (
462+
id INTEGER PRIMARY KEY,
463+
price REAL NOT NULL CHECK (price > 0),
464+
tax_rate REAL NOT NULL DEFAULT 0.1 CHECK (tax_rate >= 0 AND tax_rate <= 1),
465+
total_price REAL GENERATED ALWAYS AS (price * (1 + tax_rate)) STORED
466+
)
467+
`);
468+
469+
// Valid insert
470+
const result = db
471+
.prepare("INSERT INTO products (price, tax_rate) VALUES (100, 0.2)")
472+
.run();
473+
expect(result.changes).toBe(1);
474+
475+
// Verify generated column
476+
const product = db
477+
.prepare("SELECT * FROM products WHERE id = ?")
478+
.get(result.lastInsertRowid);
479+
expect(product.total_price).toBeCloseTo(120, 2);
480+
481+
// Cannot directly set generated column
482+
expect(() => {
483+
db.prepare(
484+
"INSERT INTO products (price, tax_rate, total_price) VALUES (100, 0.1, 999)",
485+
).run();
486+
}).toThrow(/cannot INSERT into generated column/);
487+
488+
db.close();
489+
});
138490
});
139491

140492
describe("SQL Syntax and Logic Errors", () => {

0 commit comments

Comments
 (0)