{"id":15050548,"url":"https://github.com/shinshin86/neverchange","last_synced_at":"2025-09-21T12:11:05.264Z","repository":{"id":256333960,"uuid":"854956680","full_name":"shinshin86/neverchange","owner":"shinshin86","description":"NeverChange is a database solution for web applications using SQLite WASM and OPFS.","archived":false,"fork":false,"pushed_at":"2025-03-16T23:12:40.000Z","size":1845,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-10T02:13:56.334Z","etag":null,"topics":["database","frontend","opfs","sqlite","sqlite-wasm","wasm"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/neverchange","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shinshin86.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-09-10T03:51:02.000Z","updated_at":"2025-03-16T23:12:43.000Z","dependencies_parsed_at":"2024-10-27T06:36:31.402Z","dependency_job_id":"974e920c-46bc-471e-80f7-a5fb1ff62a60","html_url":"https://github.com/shinshin86/neverchange","commit_stats":null,"previous_names":["shinshin86/neverchange"],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinshin86%2Fneverchange","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinshin86%2Fneverchange/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinshin86%2Fneverchange/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shinshin86%2Fneverchange/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shinshin86","download_url":"https://codeload.github.com/shinshin86/neverchange/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248142903,"owners_count":21054671,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","frontend","opfs","sqlite","sqlite-wasm","wasm"],"created_at":"2024-09-24T21:27:14.930Z","updated_at":"2025-09-21T12:11:05.253Z","avatar_url":"https://github.com/shinshin86.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# NeverChange\n\n[![CI](https://github.com/shinshin86/neverchange/actions/workflows/ci.yml/badge.svg)](https://github.com/shinshin86/neverchange/actions/workflows/ci.yml) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/shinshin86/neverchange)\n\n![Project logo](./images/logo.png)\n\nNeverChange is a database solution for web applications using SQLite WASM and OPFS.\n\n## 👩‍💻 Beta Version Warning 👨‍💻\n\n**This package is currently in beta stage. The interface and method names may change with each update. Please use with caution.**\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Supported Browsers](#supported-browsers)\n- [About Application Deployment](#about-application-deployment)\n  - [Netlify (Recommendation)](#netlify-recommendation)\n  - [Cloudflare Pages (Recommendation)](#cloudflare-pages-recommendation)\n  - [GitHub Pages](#github-pages)\n- [Requirements](#requirements)\n- [Usage](#usage)\n  - [Basic](#basic)\n  - [Migration](#migration)\n  - [Dump and Import Features](#dump-and-import-features)\n  - [CSV Export and Import](#csv-export-and-import)\n- [For Developers](#for-developers)\n  - [Setup](#setup)\n  - [Available Scripts](#available-scripts)\n  - [Main Dependencies](#main-dependencies)\n  - [Development](#development)\n- [License](#license)\n\n## Installation\n\n```\nnpm install neverchange\n```\n\n## Supported Browsers\n\nAll end-to-end tests are conducted with Playwright in Chrome, Edge, Chromium, and Firefox.\n\n* Google Chrome\n* Microsoft Edge\n* Firefox\n* Safari\n  * (However, there are restrictions. See [About Application Deployment](#about-application-deployment) for more information.)\n\n## About Application Deployment\nOur recommendation is to deploy with Netlify for better compatibility. See below for Netlify and GitHub Pages deployment instructions.\n\n\nA full deployment guide is available in the [Deployment Documentation](docs/deployment.md), including specific configuration examples.\n\n### Netlify (Recommendation)\n\nNetlify is recommended for deploying apps with persistent data on the web front end using NeverChange.\n\nThe following statement in the `_headers` file can be used for this purpose.\n\n```\n/*  \n  Cross-Origin-Opener-Policy: same-origin\n  Cross-Origin-Embedder-Policy: require-corp\n```\n\n### Cloudflare Pages (Recommendation)\n\nCloudflare Pages is recommended for deploying apps with persistent data on the web front end using NeverChange.\n\nThe following statement in the `_headers` file can be used for this purpose.\n\n```\n/*\n  Cross-Origin-Opener-Policy: same-origin\n  Cross-Origin-Embedder-Policy: require-corp\n  Cross-Origin-Resource-Policy: same-origin\n```\n\n### GitHub Pages\nIf you want to host on GitHub Pages, you will need [coi-serviceworker.js](https://github.com/gzuidhof/coi-serviceworker).\n\nBut, Safari does not function properly in this case. Refer to the following issue for more details.  \nhttps://github.com/shinshin86/neverchange/issues/6\n\nexample:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003ctitle\u003eNeverChange Example\u003c/title\u003e\n  \u003c!-- If you want to host on GitHub, you will need coi-serviceworker.js --\u003e\n  \u003cscript src=\"/coi-serviceworker.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cdiv id=\"root\"\u003e\u003c/div\u003e\n  \u003cscript type=\"module\" src=\"/src/main.tsx\"\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nA full deployment guide is available in the [Deployment Documentation](docs/deployment.md), including specific configuration examples.\n\n## Requirements\n\n- Node.js (version 20 or higher recommended)\n- npm (usually comes with Node.js)\n\n## Usage\n\n![Usage image](./images/Usage.jpg)\n\nHere’s how to use NeverChange with some practical examples.\n\nIf you’re interested in writing SQL efficiently with NeverChange, you may also want to check out [sqlc-gen-typescript-for-neverchange](https://github.com/shinshin86/sqlc-gen-typescript-for-neverchange), which generates TypeScript code using [sqlc](https://github.com/sqlc-dev/sqlc).\n\n### Basic\n\nHere's a basic example of how to use NeverChangeDB to create a database, insert data, and query it:\n\n```typescript\nimport { NeverChangeDB } from 'neverchange';\n\nasync function main() {\n  // Initialize the database\n  const db = new NeverChangeDB('myDatabase');\n  await db.init();\n\n  // Create a table\n  await db.execute(`\n    CREATE TABLE IF NOT EXISTS users (\n      id INTEGER PRIMARY KEY AUTOINCREMENT,\n      name TEXT NOT NULL,\n      email TEXT UNIQUE NOT NULL\n    )\n  `);\n\n  // Insert data\n  await db.execute(\n    'INSERT INTO users (name, email) VALUES (?, ?)',\n    ['John Doe', 'john@example.com']\n  );\n\n  // Query data\n  const users = await db.query('SELECT * FROM users');\n  console.log('Users:', users);\n\n  // Close the database connection\n  await db.close();\n}\n\nmain().catch(console.error);\n```\n\n### Constructor Options\n\nNeverChangeDB constructor accepts an optional second parameter for configuration:\n\n```typescript\nconst db = new NeverChangeDB('myDatabase', {\n  debug: false,           // Enable debug logging (default: false)\n  isMigrationActive: true // Enable automatic migration system (default: true)\n});\n```\n\n#### Options:\n\n- **`debug`**: When set to `true`, enables detailed logging of database operations. Useful for development and debugging.\n- **`isMigrationActive`**: When set to `true` (default), the migration system is automatically enabled. When `false`, migrations are disabled and you need to manage schema changes manually.\n\n#### Example with disabled migrations:\n\n```typescript\n// Database without automatic migration system\nconst db = new NeverChangeDB('myDatabase', { \n  isMigrationActive: false,\n  debug: true \n});\nawait db.init();\n\n// You'll need to create tables manually\nawait db.execute(`\n  CREATE TABLE IF NOT EXISTS users (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    name TEXT NOT NULL,\n    email TEXT UNIQUE NOT NULL\n  )\n`);\n```\n\n### Migration\n\nNeverChangeDB supports database migrations, allowing you to evolve your database schema over time. Here's an example of how to define and use migrations:\n\n```typescript\nimport { NeverChangeDB } from 'neverchange';\n\n// Define migrations\nconst migrations = [\n  {\n    version: 1,\n    up: async (db) =\u003e {\n      await db.execute(`\n        CREATE TABLE users (\n          id INTEGER PRIMARY KEY AUTOINCREMENT,\n          name TEXT NOT NULL\n        )\n      `);\n    }\n  },\n  {\n    version: 2,\n    up: async (db) =\u003e {\n      await db.execute(`\n        ALTER TABLE users ADD COLUMN email TEXT\n      `);\n    }\n  }\n];\n\nasync function main() {\n  // Initialize the database with migrations\n  const db = new NeverChangeDB('myDatabase', { isMigrationActive: true });\n  db.addMigrations(migrations);\n  await db.init();\n\n  // The database will now have the latest schema\n  const tableInfo = await db.query('PRAGMA table_info(users)');\n  console.log('Users table schema:', tableInfo);\n\n  await db.close();\n}\n\nmain().catch(console.error);\n```\n\n### Transactions\n\nNeverChangeDB supports transaction handling.  \nWhen you call `db.transaction(async (tx) =\u003e { ... })`, the statements within the callback are wrapped in a transaction.  \n- If the callback completes successfully, the transaction is committed automatically.  \n- If an error occurs or if you explicitly call `tx.rollback()`, the transaction is rolled back.\n\n#### Basic Example\n\n```ts\nimport { NeverChangeDB } from 'neverchange';\n\nasync function main() {\n  const db = new NeverChangeDB('myDatabase');\n  await db.init();\n\n  await db.execute(`\n    CREATE TABLE IF NOT EXISTS accounts (\n      id INTEGER PRIMARY KEY AUTOINCREMENT,\n      name TEXT NOT NULL,\n      balance INTEGER NOT NULL\n    )\n  `);\n\n  // Perform multiple operations in a single transaction\n  await db.transaction(async (tx) =\u003e {\n    await tx.execute(\"INSERT INTO accounts (name, balance) VALUES (?, ?)\", [\"Alice\", 1000]);\n    await tx.execute(\"INSERT INTO accounts (name, balance) VALUES (?, ?)\", [\"Bob\", 500]);\n    // The transaction is automatically committed when this callback finishes successfully\n  });\n\n  const result = await db.query(\"SELECT * FROM accounts\");\n  console.log(result);\n\n  await db.close();\n}\n\nmain().catch(console.error);\n```\n\n#### Explicit Rollback\n\nYou can call `tx.rollback()` at any point within the transaction callback to force an immediate rollback.\n`rollback()` throws an error internally, which interrupts the rest of the transaction logic.\n\n```ts\nawait db.transaction(async (tx) =\u003e {\n  const [account] = await tx.query\u003c{ balance: number }\u003e(\n    \"SELECT balance FROM accounts WHERE name = ?\",\n    [\"Alice\"]\n  );\n\n  if (account.balance \u003c 100) {\n    // If there's not enough balance, roll back the transaction here\n    await tx.rollback();\n  }\n\n  await tx.execute(\"UPDATE accounts SET balance = balance - 100 WHERE name = ?\", [\"Alice\"]);\n  await tx.execute(\"UPDATE accounts SET balance = balance + 100 WHERE name = ?\", [\"Bob\"]);\n});\n```\n\n#### Nested Transactions (Savepoints)\nNeverChangeDB also supports nested transactions. When `db.transaction` is called inside another transaction,\nit creates a new savepoint, so an inner failure rolls back only the nested portion.\n\n```ts\nawait db.transaction(async (tx) =\u003e {\n  // Insert within top-level transaction\n  await tx.execute(\"INSERT INTO accounts (name, balance) VALUES (?, ?)\", [\"Charlie\", 200]);\n\n  try {\n    // Begin a nested transaction (savepoint)\n    await tx.transaction(async (tx2) =\u003e {\n      await tx2.execute(\"INSERT INTO accounts (name, balance) VALUES (?, ?)\", [\"David\", 300]);\n      // Force an error in the nested transaction\n      throw new Error(\"Error in nested transaction!\");\n    });\n  } catch (err) {\n    console.warn(\"Nested transaction error:\", err);\n    // Only the nested changes are rolled back\n  }\n\n  // Outer transaction can still proceed\n  await tx.execute(\"INSERT INTO accounts (name, balance) VALUES (?, ?)\", [\"Eve\", 400]);\n});\n```\n\nIn the above scenario, the insert for “David” will be rolled back due to the error in the nested transaction,\nbut the outer transaction remains valid and will commit the other statements unless it encounters its own error.\n\n#### Returning Values\n\nIf your callback returns a value, that value is also returned from the `transaction` method:\n\n```ts\nconst updatedBalance = await db.transaction(async (tx) =\u003e {\n  await tx.execute(\"UPDATE accounts SET balance = balance - 50 WHERE name = ?\", [\"Alice\"]);\n  const [row] = await tx.query\u003c{ balance: number }\u003e(\n    \"SELECT balance FROM accounts WHERE name = ?\",\n    [\"Alice\"]\n  );\n  return row.balance;\n});\n\nconsole.log(\"Alice's updated balance:\", updatedBalance);\n```\n\n#### Manual `commit()` and `rollback()` Methods\nWhile `transaction` automatically handles commits and rollbacks,\nyou can also manually call `db.commit()` or `db.rollback()` **inside an active transaction** if needed:\n\n```ts\n// Manual rollback example\nawait db.transaction(async (tx) =\u003e {\n  await tx.execute(\"INSERT INTO accounts (name, balance) VALUES (?, ?)\", [\"Test\", 100]);\n  \n  // Some condition check\n  const shouldRollback = true;\n  if (shouldRollback) {\n    await tx.rollback(); // Explicitly rollback the transaction\n  }\n});\n\n// Manual commit example (though auto-commit is usually preferred)\nawait db.transaction(async (tx) =\u003e {\n  await tx.execute(\"INSERT INTO accounts (name, balance) VALUES (?, ?)\", [\"Test\", 100]);\n  await tx.commit(); // Explicitly commit the transaction\n});\n```\n\nIn nested scenarios, these methods map to `RELEASE SAVEPOINT` or `ROLLBACK TO SAVEPOINT`, respectively.\n**Note**: If you manually execute `BEGIN TRANSACTION` (via `db.execute(\"BEGIN TRANSACTION\")`),\nit will not increment the internal transaction depth. \nThus, calling `db.commit()` or `db.rollback()` after a manual `BEGIN` will cause an error\n(because the library doesn't recognize it as an active transaction).\n\n### Dump and Import Features\n\nNeverChangeDB offers two modes for database dump and import: Optimized Mode and SQLite Compatibility Mode.\n\n#### Optimized Mode (Default)\n\nIn the optimized mode, the dump output does not include transaction control statements or PRAGMA settings. This mode is designed for:\n\n- Flexibility: Allows for custom transaction control during import.\n- Consistency: Ensures the entire import process is wrapped in a single transaction.\n- Error Handling: Facilitates easy rollback in case of import errors.\n- Performance: Enables fine-tuned control over transaction size and checkpoints for large datasets.\n- Platform Independence: Improves compatibility between different SQLite implementations.\n\n#### SQLite Compatibility Mode\n\nThis mode generates dump output that closely resembles the standard SQLite `.dump` command, including transaction control statements and PRAGMA settings. Use this mode when:\n\n- Compatibility with standard SQLite tools is required.\n- You need to use the dump with other systems expecting standard SQLite dump format.\n\n#### Examples\n\n```typescript\n// Dumping a Database\nconst db = new NeverChangeDB('myDatabase');\nawait db.init();\n\n// Optimized Mode (default) - dump entire database\nconst optimizedDump = await db.dumpDatabase();\n\n// SQLite Compatibility Mode - dump entire database\nconst compatibleDump = await db.dumpDatabase({ compatibilityMode: true });\n\n// Dump specific table only\nconst tableDump = await db.dumpDatabase({ table: 'users' });\n\n// Dump specific table with compatibility mode\nconst tableCompatibleDump = await db.dumpDatabase({ \n  table: 'users', \n  compatibilityMode: true \n});\n\n// Importing a Database\n// Optimized Mode (default)\nawait db.importDump(dumpContent);\n\n// SQLite Compatibility Mode\nawait db.importDump(dumpContent, { compatibilityMode: true });\n```\n\n#### Handling of BLOB Data\n\nWhen using the dump and import features, special attention should be paid to BLOB (Binary Large Object) data:\n\n- **Dumping BLOB Data**: BLOB data is serialized into a special string format during the dump process. This ensures that binary data is correctly represented in the dump output.\n\n- **Importing BLOB Data**: When importing, the special string format for BLOB data is automatically detected and converted back into the appropriate binary format.\n\n- **Working with BLOB Data**: After importing, BLOB data may be represented as an object with numeric keys (e.g., `{\"0\":1,\"1\":2,\"2\":3}`). To work with this data as a `Uint8Array`, you may need to convert it:\n\n```javascript\nconst convertToUint8Array = (obj) =\u003e {\n   if (obj \u0026\u0026 typeof obj === 'object' \u0026\u0026 !Array.isArray(obj)) {\n      return new Uint8Array(Object.values(obj));\n   }\n\n   return obj;\n};\n\n// Usage\nconst blobData = convertToUint8Array(row.blobColumn);\n```\n\n#### Limitations and Considerations\n\n - **Large Databases**: When working with large databases, consider the memory limitations of the browser environment. For very large datasets, you may need to implement chunking strategies for dump and import operations.\n - **Complex Data Types**: While NeverChangeDB handles most SQLite data types seamlessly, complex types like JSON or custom data structures may require additional processing when dumping or importing.\n - **Cross-Browser Compatibility**: Although the core functionality is designed to work across modern browsers, some advanced features or performance optimizations may vary between different browser environments. Always test thoroughly in your target browsers.\n\n### CSV Export and Import\n\nNeverChangeDB also supports CSV export and import functionality, which is useful for integrating with spreadsheet applications and external data sources.\n\n#### CSV Export\n\nYou can export a table to a CSV format using the `dumpTableToCSV` method:\n\n```typescript\nconst db = new NeverChangeDB('myDatabase');\nawait db.init();\n\n/* We will assume that you have added tables and information */\n\n// Basic CSV export\nconst csvContent = await db.dumpTableToCSV('your_table');\nconsole.log('CSV Export:', csvContent);\n\n// CSV export with all fields quoted\nconst quotedCsvContent = await db.dumpTableToCSV('your_table', { \n  quoteAllFields: true \n});\nconsole.log('Quoted CSV Export:', quotedCsvContent);\n\nawait db.close();\n```\n\nThis will export the contents of `your_table` to a CSV string. The `quoteAllFields` option allows you to force all fields to be quoted, which can be useful for ensuring compatibility with certain CSV parsers.\n\n#### CSV Import\n\nYou can import CSV content into a table using the `importCSVToTable` method:\n\n```typescript\nconst db = new NeverChangeDB('myDatabase');\nawait db.init();\n\nconst csvContent = `id,name,email\\n1,John Doe,john@example.com\\n2,Jane Smith,jane@example.com`;\nawait db.importCSVToTable('your_table', csvContent);\n\nawait db.close();\n```\n\nThis will insert the CSV data into the `your_table` table. Ensure the table is created beforehand and the columns match the CSV headers.\n\n## For Developers\n\n![For Developers image](./images/for-developers-image.jpg)\n\n### Setup\n\n1. Clone the repository:\n   ```\n   git clone https://github.com/shinshin86/neverchange.git\n   cd neverchange\n   ```\n\n2. Install dependencies:\n   ```\n   npm install\n   ```\n\n3. Install browsers for Playwright for e2e test:\n   ```\n   npx playwright install\n   ```\n\n### Available Scripts\n\n- `npm run build`: Build the project.\n- `npm run dev:e2e`: Start the development server for E2E tests.\n- `npm run e2e`: Run E2E tests using Playwright.\n\n### Main Dependencies\n\n- [@sqlite.org/sqlite-wasm](https://www.npmjs.com/package/@sqlite.org/sqlite-wasm): SQLite WASM implementation\n- [Vite](https://vitejs.dev/): Fast frontend build tool\n- [TypeScript](https://www.typescriptlang.org/): Typed superset of JavaScript\n- [Playwright](https://playwright.dev/): Modern web testing and automation framework\n\n### Development\n\n**Run E2E tests:**\n```\nnpm run e2e\n```\n\nIn CI tests, `Chrome` and `Edge` do not work, but you can test locally by adding the following settings.  \n(https://github.com/shinshin86/neverchange/issues/12)\n\n```diff\n--- a/playwright.config.ts\n+++ b/playwright.config.ts\n@@ -12,6 +12,22 @@ export default defineConfig({\n     trace: 'on-first-retry',\n   },\n   projects: [\n+    {\n+      name: 'Google Chrome',\n+      use: {\n+        ...devices['Desktop Chrome'],\n+        channel: 'chrome',\n+        headless: false,\n+      },\n+    },\n+    {\n+      name: 'Microsoft Edge',\n+      use: {\n+        ...devices['Desktop Edge'],\n+        channel: 'msedge',\n+        headless: false,\n+      },\n+    },\n     {\n       name: 'chromium',\n       use: {\n```\n\n**Code Format:**\n\n```\nnpm run fmt\n```\n\n## License\n\nThis project is released under the [MIT License](LICENSE).","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshinshin86%2Fneverchange","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshinshin86%2Fneverchange","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshinshin86%2Fneverchange/lists"}