@@ -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 ( / A U T O I N C R E M E N T .* o n l y .* I N T E G E R P R I M A R Y K E Y / 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 ( / s y n t a x e r r o r | A U T O I N C R E M E N T / 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 ( / s y n t a x e r r o r | n e a r .* A U T O I N C R E M E N T / 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 ( / N O T N U L L c o n s t r a i n t f a i l e d / ) ;
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 ( / N O T N U L L c o n s t r a i n t f a i l e d / ) ;
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 ( / C H E C K c o n s t r a i n t f a i l e d / ) ;
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 ( / U N I Q U E c o n s t r a i n t f a i l e d / ) ;
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 ( / C H E C K c o n s t r a i n t f a i l e d / ) ;
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 ( / F O R E I G N K E Y c o n s t r a i n t f a i l e d / ) ;
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 ( / F O R E I G N K E Y c o n s t r a i n t f a i l e d / ) ;
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 ( / U N I Q U E c o n s t r a i n t f a i l e d / ) ;
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 ( / c a n n o t I N S E R T i n t o g e n e r a t e d c o l u m n / ) ;
487+
488+ db . close ( ) ;
489+ } ) ;
138490 } ) ;
139491
140492 describe ( "SQL Syntax and Logic Errors" , ( ) => {
0 commit comments