export function createSqliteAdapter(context) { const { mkdir, dirname, sqliteSchema, mysqlSchema, optional, buildSeedOperations, stateFromRows, readSqliteRows, readMysqlRows, createRepository } = context; async function createSqliteStore({ path, seed }) { const { DatabaseSync } = await import('node:sqlite'); await mkdir(dirname(path), { recursive: true }); const connection = new DatabaseSync(path, { timeout: 5000 }); connection.exec('PRAGMA journal_mode = WAL;'); connection.exec('PRAGMA synchronous = NORMAL;'); const tableExists = name => Boolean(connection.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)); const ensureColumns = (table, columns) => { if (!tableExists(table)) return; const existing = new Set(connection.prepare(`PRAGMA table_info(${table})`).all().map(item => item.name)); for (const [name, definition] of columns) { if (!existing.has(name)) connection.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`); } }; ensureColumns('users', [ ['admin_level', 'TEXT'], ['school_id', 'TEXT'], ['class_id', 'TEXT'], ['active', 'INTEGER NOT NULL DEFAULT 1'], ['candidate_number', 'TEXT'], ['must_change_password', 'INTEGER NOT NULL DEFAULT 0'], ['archived_at', 'TEXT'], ['archived_by', 'TEXT'] ]); ensureColumns('schema_metadata', [['self_registration_enabled', 'INTEGER NOT NULL DEFAULT 0']]); ensureColumns('candidate_profiles', [ ['school_id', 'TEXT'], ['class_id', 'TEXT'], ['native_place', 'TEXT'], ['birth_date', 'TEXT'], ['ethnicity', 'TEXT'], ['postal_code', 'TEXT'], ['guardian_name', 'TEXT'], ['guardian_phone', 'TEXT'], ['profile_completed', 'INTEGER NOT NULL DEFAULT 0'], ['province_code', 'TEXT'], ['province_name', 'TEXT'], ['city_code', 'TEXT'], ['city_name', 'TEXT'], ['district_code', 'TEXT'], ['district_name', 'TEXT'] ]); ensureColumns('registrations', [['registration_number', 'TEXT'], ['number_rule_id', 'TEXT']]); ensureColumns('exams', [ ['pass_policy', "TEXT NOT NULL DEFAULT 'rank_percent'"], ['pass_value', 'REAL NOT NULL DEFAULT 60'], ['archived_at', 'TEXT'], ['archived_by', 'TEXT'] ]); ensureColumns('exam_subjects', [ ['full_score', 'REAL NOT NULL DEFAULT 150'], ['pass_score', 'REAL NOT NULL DEFAULT 90'], ['pass_rule', "TEXT NOT NULL DEFAULT 'fixed_score'"], ['pass_value', 'REAL NOT NULL DEFAULT 90'] ]); ensureColumns('test_centers', [ ['code', 'TEXT'], ['manager_name', 'TEXT'], ['manager_phone', 'TEXT'], ['emergency_phone', 'TEXT'], ['gate_open_time', 'TEXT'], ['transport', 'TEXT'], ['status', "TEXT NOT NULL DEFAULT 'active'"], ['notes', 'TEXT'], ['province_code', 'TEXT'], ['province_name', 'TEXT'], ['city_code', 'TEXT'], ['city_name', 'TEXT'], ['district_code', 'TEXT'], ['district_name', 'TEXT'] ]); ensureColumns('center_change_requests', [ ['province_code', 'TEXT'], ['province_name', 'TEXT'], ['city_code', 'TEXT'], ['city_name', 'TEXT'], ['district_code', 'TEXT'], ['district_name', 'TEXT'] ]); ensureColumns('test_rooms', [['seat_plan', 'TEXT']]); ensureColumns('center_change_rooms', [['seat_plan', 'TEXT']]); ensureColumns('admit_cards', [ ['center_code', "TEXT NOT NULL DEFAULT ''"], ['center_address', "TEXT NOT NULL DEFAULT ''"] ]); ensureColumns('admit_card_subjects', [ ['building', "TEXT NOT NULL DEFAULT ''"], ['floor', "TEXT NOT NULL DEFAULT ''"] ]); if (tableExists('workflow_definitions')) { const definitionSql = connection.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'workflow_definitions'").get()?.sql || ''; if (!definitionSql.includes('candidate_account_batch')) { connection.exec(` PRAGMA foreign_keys = OFF; BEGIN IMMEDIATE; CREATE TABLE workflow_definitions_v5 ( id TEXT PRIMARY KEY, business_type TEXT NOT NULL CHECK (business_type IN ('profile_change', 'registration_review', 'center_change', 'candidate_account_batch')), name TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)), updated_by TEXT REFERENCES users(id) ON DELETE SET NULL, updated_at TEXT NOT NULL, UNIQUE (business_type, active) ) STRICT; INSERT INTO workflow_definitions_v5 (id, business_type, name, active, updated_by, updated_at) SELECT id, business_type, name, active, updated_by, updated_at FROM workflow_definitions; DROP TABLE workflow_definitions; ALTER TABLE workflow_definitions_v5 RENAME TO workflow_definitions; COMMIT; PRAGMA foreign_keys = ON; `); } } if (tableExists('registrations')) { const registrationsSql = connection.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'registrations'").get()?.sql || ''; if (/registration_number\s+TEXT\s+UNIQUE/i.test(registrationsSql)) { connection.exec(` PRAGMA foreign_keys = OFF; BEGIN IMMEDIATE; CREATE TABLE registrations_v4 ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, exam_id TEXT NOT NULL REFERENCES exams(id) ON DELETE CASCADE, status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'rejected')), payment_status TEXT NOT NULL CHECK (payment_status IN ('unpaid', 'paid', 'refunded')), created_at TEXT NOT NULL, reviewed_at TEXT, review_note TEXT, registration_number TEXT, number_rule_id TEXT, UNIQUE (user_id, exam_id) ) STRICT; INSERT INTO registrations_v4 ( id, user_id, exam_id, status, payment_status, created_at, reviewed_at, review_note, registration_number, number_rule_id ) SELECT id, user_id, exam_id, status, payment_status, created_at, reviewed_at, review_note, registration_number, number_rule_id FROM registrations; DROP TABLE registrations; ALTER TABLE registrations_v4 RENAME TO registrations; COMMIT; PRAGMA foreign_keys = ON; `); } } connection.exec(sqliteSchema); connection.exec('CREATE UNIQUE INDEX IF NOT EXISTS uq_users_candidate_number ON users(candidate_number)'); const existingSystem = connection.prepare('SELECT * FROM schema_metadata WHERE id = 1').get(); if (existingSystem && Number(existingSystem.schema_version || 1) < 9) { const extension = seed(); connection.exec('PRAGMA foreign_keys = OFF;'); connection.exec('BEGIN IMMEDIATE'); try { connection.exec(` DROP TABLE IF EXISTS admit_card_subjects; DROP TABLE IF EXISTS admit_cards; DROP TABLE IF EXISTS exam_arrangement_plans; DROP TABLE IF EXISTS admission_number_rules; CREATE TABLE admission_number_rules ( id TEXT PRIMARY KEY, code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, description TEXT NOT NULL, separator TEXT NOT NULL DEFAULT '', segments_json TEXT NOT NULL, example TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)), created_at TEXT NOT NULL ) STRICT; CREATE TABLE exam_arrangement_plans ( id TEXT PRIMARY KEY, exam_id TEXT NOT NULL UNIQUE REFERENCES exams(id) ON DELETE CASCADE, number_rule_id TEXT NOT NULL REFERENCES admission_number_rules(id), mixing_scope TEXT NOT NULL CHECK (mixing_scope IN ('class', 'school', 'district', 'city', 'province')), random_seed TEXT NOT NULL, candidate_count INTEGER NOT NULL CHECK (candidate_count >= 0), center_count INTEGER NOT NULL CHECK (center_count >= 0), subject_assignment_count INTEGER NOT NULL CHECK (subject_assignment_count >= 0), subject_combination_count INTEGER NOT NULL CHECK (subject_combination_count >= 0), same_school_center_rate REAL NOT NULL, warnings_json TEXT NOT NULL, generated_by TEXT REFERENCES users(id) ON DELETE SET NULL, generated_at TEXT NOT NULL ) STRICT; CREATE TABLE admit_cards ( registration_id TEXT PRIMARY KEY REFERENCES registrations(id) ON DELETE CASCADE, plan_id TEXT NOT NULL REFERENCES exam_arrangement_plans(id) ON DELETE CASCADE, card_number TEXT NOT NULL UNIQUE, center_id TEXT REFERENCES test_centers(id) ON DELETE SET NULL, test_center TEXT NOT NULL, center_code TEXT NOT NULL, center_address TEXT NOT NULL, generated_at TEXT NOT NULL ) STRICT; CREATE TABLE admit_card_subjects ( registration_id TEXT NOT NULL REFERENCES admit_cards(registration_id) ON DELETE CASCADE, subject_id TEXT NOT NULL REFERENCES exam_subjects(id) ON DELETE CASCADE, room_id TEXT REFERENCES test_rooms(id) ON DELETE SET NULL, room TEXT NOT NULL, room_code TEXT NOT NULL, exam_room_code TEXT NOT NULL, building TEXT NOT NULL, floor TEXT NOT NULL, seat TEXT NOT NULL, subject_signature TEXT NOT NULL, PRIMARY KEY (registration_id, subject_id), UNIQUE (subject_id, room_id, seat) ) STRICT; CREATE INDEX idx_arrangement_plans_exam ON exam_arrangement_plans(exam_id, generated_at); CREATE INDEX idx_admit_subjects_room ON admit_card_subjects(subject_id, room_id, seat); `); for (const rule of extension.admissionNumberRules) connection.prepare( `INSERT INTO admission_number_rules ( id, code, name, description, separator, segments_json, example, active, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run(rule.id, rule.code, rule.name, rule.description, rule.separator || '', JSON.stringify(rule.segments || []), rule.example || '', rule.active === false ? 0 : 1, rule.createdAt); connection.prepare('UPDATE schema_metadata SET schema_version = 9, app_version = 9 WHERE id = 1').run(); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); connection.close(); throw error; } finally { try { connection.exec('PRAGMA foreign_keys = ON;'); } catch {} } } if (existingSystem && Number(existingSystem.schema_version || 1) < 10) { connection.prepare(` UPDATE admit_cards SET center_code = COALESCE((SELECT code FROM test_centers WHERE id = admit_cards.center_id), center_code), center_address = COALESCE((SELECT trim( COALESCE(province_name, '') || ' ' || COALESCE(city_name, '') || ' ' || COALESCE(district_name, '') || ' ' || COALESCE(address, '') ) FROM test_centers WHERE id = admit_cards.center_id), center_address) `).run(); connection.prepare(` UPDATE admit_card_subjects SET building = COALESCE((SELECT building FROM test_rooms WHERE id = admit_card_subjects.room_id), building), floor = COALESCE((SELECT floor FROM test_rooms WHERE id = admit_card_subjects.room_id), floor) `).run(); connection.prepare('UPDATE schema_metadata SET schema_version = 10, app_version = 10 WHERE id = 1').run(); } if (existingSystem && Number(existingSystem.schema_version || 1) < 11) { connection.exec('PRAGMA foreign_keys = OFF;'); connection.exec('BEGIN IMMEDIATE'); try { connection.prepare("UPDATE exam_subjects SET pass_rule = 'fixed_score', pass_value = pass_score").run(); connection.exec(` CREATE TABLE results_v11 ( id TEXT PRIMARY KEY, registration_id TEXT NOT NULL REFERENCES registrations(id) ON DELETE CASCADE, subject_id TEXT NOT NULL REFERENCES exam_subjects(id) ON DELETE CASCADE, score REAL NOT NULL CHECK (score >= 0), grade TEXT NOT NULL, published INTEGER NOT NULL DEFAULT 0 CHECK (published IN (0, 1)), updated_at TEXT, published_at TEXT, UNIQUE (registration_id, subject_id) ) STRICT; INSERT INTO results_v11 (id, registration_id, subject_id, score, grade, published, updated_at, published_at) SELECT id, registration_id, subject_id, score, grade, published, updated_at, published_at FROM results; DROP TABLE results; ALTER TABLE results_v11 RENAME TO results; CREATE INDEX IF NOT EXISTS idx_results_registration ON results(registration_id, published); `); connection.prepare('UPDATE schema_metadata SET schema_version = 11, app_version = 11 WHERE id = 1').run(); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); connection.close(); throw error; } finally { try { connection.exec('PRAGMA foreign_keys = ON;'); } catch {} } } if (existingSystem && Number(existingSystem.schema_version || 1) < 12) { connection.prepare("UPDATE exams SET pass_policy = 'rank_percent' WHERE pass_policy = 'score_ratio'").run(); connection.prepare('UPDATE schema_metadata SET schema_version = 12, app_version = 12 WHERE id = 1').run(); } if (existingSystem && Number(existingSystem.schema_version || 1) < 13) { connection.prepare('UPDATE schema_metadata SET schema_version = 13, app_version = 13 WHERE id = 1').run(); } if (existingSystem && Number(existingSystem.schema_version || 1) < 14) { connection.prepare('UPDATE schema_metadata SET schema_version = 14, app_version = 14 WHERE id = 1').run(); } if (existingSystem && Number(existingSystem.app_version || 1) < 2) { const extension = seed(); connection.exec('BEGIN IMMEDIATE'); try { for (const school of extension.schools) connection.prepare( 'INSERT OR IGNORE INTO schools (id, name, code, address, active) VALUES (?, ?, ?, ?, ?)' ).run(school.id, school.name, school.code, optional(school.address), school.active === false ? 0 : 1); for (const schoolClass of extension.classes) connection.prepare( 'INSERT OR IGNORE INTO school_classes (id, school_id, name, grade, active) VALUES (?, ?, ?, ?, ?)' ).run(schoolClass.id, schoolClass.schoolId, schoolClass.name, schoolClass.grade, schoolClass.active === false ? 0 : 1); connection.prepare("UPDATE users SET admin_level = COALESCE(admin_level, 'super'), active = COALESCE(active, 1) WHERE role = 'admin'").run(); for (const user of extension.users.filter(item => item.role === 'admin')) connection.prepare( `INSERT OR IGNORE INTO users ( id, username, password_hash, role, admin_level, school_id, class_id, active, display_name, created_at ) VALUES (?, ?, ?, 'admin', ?, ?, ?, ?, ?, ?)` ).run(user.id, user.username, user.passwordHash, user.adminLevel, optional(user.schoolId), optional(user.classId), user.active === false ? 0 : 1, user.displayName, user.createdAt); for (const profile of extension.candidateProfiles) connection.prepare( `UPDATE candidate_profiles SET school_id = COALESCE(school_id, ?), class_id = COALESCE(class_id, ?) WHERE school = ? AND grade = ?` ).run(optional(profile.schoolId), optional(profile.classId), profile.school, profile.grade); if (!connection.prepare('SELECT id FROM test_centers LIMIT 1').get()) { for (const center of extension.testCenters) connection.prepare( 'INSERT INTO test_centers (id, school_id, name, address, contact, rooms, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)' ).run(center.id, center.schoolId, center.name, center.address, optional(center.contact), center.rooms || '', center.updatedAt); } if (!connection.prepare('SELECT id FROM number_rules LIMIT 1').get()) { for (const rule of extension.numberRules) { connection.prepare('INSERT INTO number_rules (id, name, separator, active, created_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(rule.id, rule.name, rule.separator || '', rule.active ? 1 : 0, optional(rule.createdBy), rule.updatedAt); rule.segments.forEach((segment, index) => connection.prepare( 'INSERT INTO number_rule_segments (id, rule_id, position, type, value, width) VALUES (?, ?, ?, ?, ?, ?)' ).run(segment.id, rule.id, Number(segment.position || index + 1), segment.type, optional(segment.value), Number(segment.width || 0))); } } if (!connection.prepare('SELECT id FROM workflow_definitions LIMIT 1').get()) { for (const workflow of extension.workflows) { connection.prepare( 'INSERT INTO workflow_definitions (id, business_type, name, active, updated_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)' ).run(workflow.id, workflow.businessType, workflow.name, workflow.active === false ? 0 : 1, optional(workflow.updatedBy), workflow.updatedAt); workflow.steps.forEach((step, index) => connection.prepare( 'INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level) VALUES (?, ?, ?, ?, ?)' ).run(step.id, workflow.id, Number(step.position || index + 1), step.name, step.adminLevel)); } } connection.prepare('UPDATE schema_metadata SET schema_version = 2, app_version = 2 WHERE id = 1').run(); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); connection.close(); throw error; } } if (existingSystem && Number(existingSystem.app_version || 1) < 3) { const extension = seed(); connection.exec('BEGIN IMMEDIATE'); try { for (const center of extension.testCenters) connection.prepare( `UPDATE test_centers SET code = COALESCE(NULLIF(code, ''), ?), manager_name = COALESCE(manager_name, ?), manager_phone = COALESCE(manager_phone, ?), emergency_phone = COALESCE(emergency_phone, ?), gate_open_time = COALESCE(gate_open_time, ?), transport = COALESCE(transport, ?), status = COALESCE(status, 'active'), notes = COALESCE(notes, ?) WHERE id = ?` ).run(center.code, optional(center.managerName), optional(center.managerPhone), optional(center.emergencyPhone), optional(center.gateOpenTime), optional(center.transport), optional(center.notes), center.id); connection.prepare("UPDATE test_centers SET code = 'CENTER-' || substr(id, -8) WHERE code IS NULL OR code = ''").run(); if (!connection.prepare('SELECT id FROM test_rooms LIMIT 1').get()) { for (const room of extension.testRooms) connection.prepare( `INSERT INTO test_rooms ( id, center_id, code, name, building, floor, capacity, seat_start, seat_end, room_type, status, notes ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run(room.id, room.centerId, room.code, room.name, room.building, optional(room.floor), Number(room.capacity), Number(room.seatStart), Number(room.seatEnd), room.roomType, room.status, optional(room.notes)); } const centerWorkflow = extension.workflows.find(item => item.businessType === 'center_change'); if (centerWorkflow && !connection.prepare("SELECT id FROM workflow_definitions WHERE business_type = 'center_change' AND active = 1").get()) { connection.prepare( 'INSERT INTO workflow_definitions (id, business_type, name, active, updated_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)' ).run(centerWorkflow.id, centerWorkflow.businessType, centerWorkflow.name, 1, optional(centerWorkflow.updatedBy), centerWorkflow.updatedAt); for (const [index, step] of centerWorkflow.steps.entries()) connection.prepare( 'INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level) VALUES (?, ?, ?, ?, ?)' ).run(step.id, centerWorkflow.id, Number(step.position || index + 1), step.name, step.adminLevel); } connection.prepare('UPDATE schema_metadata SET schema_version = 3, app_version = 3 WHERE id = 1').run(); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); connection.close(); throw error; } } if (existingSystem && Number(existingSystem.app_version || 1) < 4) { const extension = seed(); connection.exec('BEGIN IMMEDIATE'); try { for (const user of extension.users.filter(item => item.role === 'candidate')) connection.prepare( `UPDATE users SET candidate_number = COALESCE(NULLIF(candidate_number, ''), ?), must_change_password = COALESCE(must_change_password, ?) WHERE id = ?` ).run(optional(user.candidateNumber), user.mustChangePassword ? 1 : 0, user.id); connection.prepare(`UPDATE users SET candidate_number = COALESCE( (SELECT registration_number FROM registrations WHERE registrations.user_id = users.id AND registration_number IS NOT NULL AND registration_number <> '' ORDER BY created_at LIMIT 1), 'CAND-' || substr(id, -10) ) WHERE role = 'candidate' AND (candidate_number IS NULL OR candidate_number = '')`).run(); for (const profile of extension.candidateProfiles) connection.prepare( `UPDATE candidate_profiles SET native_place = COALESCE(native_place, ?), birth_date = COALESCE(birth_date, ?), ethnicity = COALESCE(ethnicity, ?), postal_code = COALESCE(postal_code, ?), guardian_name = COALESCE(guardian_name, ?), guardian_phone = COALESCE(guardian_phone, ?), profile_completed = ? WHERE id = ?` ).run(optional(profile.nativePlace), optional(profile.birthDate), optional(profile.ethnicity), optional(profile.postalCode), optional(profile.guardianName), optional(profile.guardianPhone), profile.profileCompleted ? 1 : 0, profile.id); connection.prepare(`UPDATE registrations SET registration_number = ( SELECT candidate_number FROM users WHERE users.id = registrations.user_id ) WHERE registration_number IS NULL OR registration_number = ''`).run(); connection.prepare('UPDATE schema_metadata SET schema_version = 4, app_version = 4 WHERE id = 1').run(); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); connection.close(); throw error; } } if (existingSystem && Number(existingSystem.app_version || 1) < 5) { const extension = seed(); const batchWorkflow = extension.workflows.find(item => item.businessType === 'candidate_account_batch'); connection.exec('BEGIN IMMEDIATE'); try { if (batchWorkflow && !connection.prepare("SELECT id FROM workflow_definitions WHERE business_type = 'candidate_account_batch' AND active = 1").get()) { connection.prepare( 'INSERT INTO workflow_definitions (id, business_type, name, active, updated_by, updated_at) VALUES (?, ?, ?, ?, ?, ?)' ).run(batchWorkflow.id, batchWorkflow.businessType, batchWorkflow.name, 1, optional(batchWorkflow.updatedBy), batchWorkflow.updatedAt); for (const [index, step] of batchWorkflow.steps.entries()) connection.prepare( 'INSERT INTO workflow_steps (id, workflow_id, position, name, admin_level) VALUES (?, ?, ?, ?, ?)' ).run(step.id, batchWorkflow.id, Number(step.position || index + 1), step.name, step.adminLevel); } connection.prepare('UPDATE schema_metadata SET schema_version = 5, app_version = 5 WHERE id = 1').run(); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); connection.close(); throw error; } } if (existingSystem && Number(existingSystem.app_version || 1) < 6) { connection.prepare('UPDATE schema_metadata SET schema_version = 6, app_version = 6 WHERE id = 1').run(); } if (existingSystem && Number(existingSystem.app_version || 1) < 7) { connection.prepare('UPDATE schema_metadata SET schema_version = 7, app_version = 7 WHERE id = 1').run(); } if (existingSystem && Number(existingSystem.schema_version || 1) < 14) { connection.prepare('UPDATE schema_metadata SET schema_version = 14, app_version = 14 WHERE id = 1').run(); } if (!connection.prepare('SELECT id FROM schema_metadata WHERE id = 1').get()) { const initialState = seed(); connection.exec('BEGIN IMMEDIATE'); try { connection.prepare(` INSERT INTO schema_metadata (id, schema_version, app_version, self_registration_enabled, created_at) VALUES (1, 14, ?, ?, ?) `).run(Number(initialState.meta?.version || 1), initialState.settings?.selfRegistrationEnabled ? 1 : 0, initialState.meta?.createdAt || new Date().toISOString()); for (const item of buildSeedOperations(initialState)) connection.prepare(item.sql).run(...item.params); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); connection.close(); throw error; } } const transaction = async operations => { connection.exec('BEGIN IMMEDIATE'); try { for (const item of operations) connection.prepare(item.sql).run(...item.params); connection.exec('COMMIT'); } catch (error) { connection.exec('ROLLBACK'); throw error; } }; return createRepository({ client: 'sqlite', location: path, read: async () => stateFromRows(readSqliteRows(connection)), transaction, close: async () => connection.close() }); } return createSqliteStore; }