From 377acdb108d2046e7bcaaedf28ed4291495723b5 Mon Sep 17 00:00:00 2001 From: Alexey Suzdalenko <62180604+suzdalenko-dev@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:10:43 +0200 Subject: [PATCH 1/2] Improve Marketplace discovery and branding --- package.json | 31 ++++++++++++++++++++++------ resources/simple-db-marketplace.png | Bin 0 -> 64115 bytes 2 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 resources/simple-db-marketplace.png diff --git a/package.json b/package.json index d91c734..f13ea1d 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "simple-db-suzdalenko", - "displayName": "Simple DB", - "description": "Cliente SQL para SQLite, PostgreSQL, MySQL, SQL Server y Oracle dentro de Visual Studio Code.", + "displayName": "Simple DB - SQL Client for SQLite, MySQL, PostgreSQL, SQL Server & Oracle", + "description": "SQL client and database editor for SQLite, MySQL, PostgreSQL, SQL Server (MSSQL) and Oracle. Browse database objects, run queries, manage DDL and export results.", "version": "0.1.1", "publisher": "suzdalenko-dev", "license": "MIT", + "icon": "resources/simple-db-marketplace.png", "repository": { "type": "git", "url": "https://github.com/suzdalenko-dev/simple-db-suzdalenko.git" @@ -24,13 +25,31 @@ "Data Science" ], "keywords": [ - "database", "sql", + "database", + "sql client", + "database client", + "sql editor", + "database editor", + "query editor", + "database explorer", "sqlite", - "postgresql", + "sqlite client", "mysql", - "sql-server", - "oracle" + "mysql client", + "postgresql", + "postgres", + "postgresql client", + "oracle", + "oracle database", + "oracle client", + "plsql", + "pl/sql", + "sql server", + "mssql", + "t-sql", + "ddl", + "schema explorer" ], "activationEvents": [ "onView:simpleDb.connections", diff --git a/resources/simple-db-marketplace.png b/resources/simple-db-marketplace.png new file mode 100644 index 0000000000000000000000000000000000000000..1445f8f73a860cda205f027780f6b971842ede30 GIT binary patch literal 64115 zcmV)QK(xP!P)^V1hB&7ze=E#$VD}zMk)9rZo`EmzWP`pJgxsVXrv7$@m)ImW~R{@vj65`yM|KeX*!uy*+&{ zzKH=c#vYEE?LKf!kYrB6E}5@Nx)few#DfZdqIkN=dR;>5w4cVWNguNGF$ltd$jt&3 z%@ZYk$WS;Li6HM7{8{J@{Bd;0F|&(jwqtG&f;oWTlp$QO&eE1C?Iar{j(Z>e=)`Yj zqi-2XWM)YK8bJrFMRhTcoP&{b(YIDtM&Y>O%a{QWv?6BCh*HAt8FozOg_xNDf-d4{ z&Jrz4(!ySw+&}+=<|9BP2`QIR@B5M7#t}CbO(zvR9&s4iVweaI07}QjY`%yKwq-R*XN4iATA82*QO^1t^w;WX3s1d*d5}F!r+aTxm+N4LCg=drb}Ob%Gnyo%J)#U(v2}C8>1@a?nVO`CDRbt;nv0Y-jl0<%EZNhO zUPQt5{0)WO;AB=fR=u&ENp(;p=calUb?gC+qKd+{E53%rj6+@x#)0^O46k4(6+g zK*G}QQo<3$0ggS)JFmyFN8ywp%V3pufO<+EVf3oVVIt!_WZXam2zWE1Z#A!f7muD# zU{(P*bo63-Ne)PWjF;tOhayk-c(YS

1$cBa>SQCF1p94dzAed1WmAx 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 Y03v`plD512RsaA107*qoM6N<$f?t%S_y7O^ literal 0 HcmV?d00001 From 717c0abc3f4b3c7a7b7603fe627a0e81b2699c53 Mon Sep 17 00:00:00 2001 From: Alexey Suzdalenko <62180604+suzdalenko-dev@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:29:36 +0200 Subject: [PATCH 2/2] Standardize project language in English --- .github/workflows/ci.yml | 18 +-- .vscode/launch.json | 2 +- CHANGELOG.md | 64 ++++---- README.md | 216 +++++++++++++-------------- package.json | 72 ++++----- plan.txt | 98 ++++++------ project requirements.txt | 108 +++++++------- src/adapters/baseAdapter.js | 4 +- src/adapters/factory.js | 2 +- src/adapters/mysqlAdapter.js | 4 +- src/adapters/oracleAdapter.js | 8 +- src/adapters/postgresqlAdapter.js | 4 +- src/adapters/sqlServerAdapter.js | 6 +- src/adapters/sqliteAdapter.js | 12 +- src/adapters/sqliteWorker.js | 24 +-- src/core/errors.js | 2 +- src/extension.js | 114 +++++++------- src/managers/connectionManager.js | 6 +- src/managers/editorSessionManager.js | 24 +-- src/services/exportService.js | 8 +- src/services/queryRunner.js | 34 ++--- src/sql/ddlTemplates.js | 86 +++++------ src/sql/transactionControl.js | 8 +- src/storage/connectionStore.js | 8 +- src/storage/historyStore.js | 2 +- src/storage/resultStore.js | 4 +- src/test/adapterFactory.test.js | 6 +- src/test/connectionStore.test.js | 10 +- src/test/databaseEngines.test.js | 6 +- src/test/ddlTemplates.test.js | 10 +- src/test/resultStore.test.js | 4 +- src/test/safety.test.js | 6 +- src/test/sqlSplitter.test.js | 40 ++--- src/test/sqliteAdapter.test.js | 30 ++-- src/test/transactionControl.test.js | 6 +- src/test/valueNormalizer.test.js | 6 +- src/test/xlsx.test.js | 6 +- src/ui/connectionForm.js | 98 ++++++------ src/views/connectionsTreeProvider.js | 48 +++--- src/views/historyTreeProvider.js | 10 +- src/views/resultPanel.js | 32 ++-- 41 files changed, 629 insertions(+), 627 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9b8d19..141d041 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Verificación +name: Verification on: push: @@ -12,32 +12,32 @@ permissions: jobs: verify: - name: Analizar, probar y empaquetar JavaScript + name: Lint, test, and package JavaScript runs-on: ubuntu-latest steps: - - name: Descargar el código + - name: Check out source uses: actions/checkout@v4 - - name: Configurar Node.js + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: npm - - name: Instalar dependencias + - name: Install dependencies run: npm ci - - name: Auditar dependencias de producción + - name: Audit production dependencies run: npm audit --omit=dev - - name: Verificar el proyecto + - name: Verify project run: npm run check - - name: Crear VSIX + - name: Build VSIX run: npm run package - - name: Guardar VSIX + - name: Upload VSIX uses: actions/upload-artifact@v4 with: name: simple-db-vsix diff --git a/.vscode/launch.json b/.vscode/launch.json index 969ac60..41936fe 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Ejecutar Simple DB", + "name": "Run Simple DB", "type": "extensionHost", "request": "launch", "args": [ diff --git a/CHANGELOG.md b/CHANGELOG.md index e68d20f..8cf93f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,40 +1,42 @@ # Changelog -Todos los cambios relevantes de Simple DB se documentan en este archivo. +All notable changes to Simple DB are documented in this file. ## 0.1.1 - 2026-08-07 -### Añadido - -- SQLite como quinto motor, ejecutado con `sql.js` en un Worker cancelable. -- Adaptadores completos para SQLite, PostgreSQL, MySQL, SQL Server y Oracle. -- Perfiles múltiples, prueba de conexión, conexión/desconexión y contraseñas en `SecretStorage`. -- Exploración de bases, esquemas y objetos específicos: tablas, vistas, vistas materializadas, rutinas, packages Oracle, índices, triggers, secuencias, tipos, sinónimos y eventos MySQL según el motor. -- Editor SQL por sesión y contexto de base/esquema. -- Parser de dialecto para PostgreSQL `$$`, MySQL `DELIMITER`, SQL Server `GO`, Oracle PL/SQL `/` y triggers SQLite. -- Ejecución de selección, sentencia actual o documento; DML, DDL y SQL arbitrario. -- Lectura/plantillas de DDL con acciones para mostrar definición, preparar `CREATE`, `ALTER` y `DROP`. -- Transacciones por editor, `COMMIT`, `ROLLBACK`, cancelación y timeouts. -- Resultados paginados en disco, múltiples result sets, copia de celda/fila/selección y exportación CSV/JSON/XLSX. -- Historial configurable, duración, filas recuperadas y filas afectadas. -- Confirmaciones configurables para operaciones destructivas y DML sin `WHERE`. -- Pruebas automatizadas íntegramente en JavaScript, incluida integración real del adaptador SQLite. -- Protección SQLite frente a cambios externos y WAL activo, además de lectura exacta de enteros de 64 bits. -- DDL específico por motor para índices/triggers y eliminación correcta de índices respaldados por constraints cuando el catálogo aporta esa información. -- Protección de inicios de transacción escritos como SQL para mantener PostgreSQL/MySQL sobre una conexión física reservada, incluidas variantes de `START TRANSACTION`. -- Protección frente a inyección de fórmulas al exportar CSV y preservación de `NUMBER` Oracle como texto exacto. - -### Cambiado - -- Proyecto migrado completamente de TypeScript a JavaScript: no quedan `.ts`, `tsconfig.json` ni compilación TypeScript. -- `F5` carga directamente `src/extension.js`. -- `simpleDb.maxRows` pasa a `0` por defecto, es decir, **sin límite de filas**. El usuario puede configurar uno si lo necesita. -- La paginación de resultados es almacenamiento/visualización y no altera el SQL con `TOP`, `LIMIT` o `FETCH`. -- CI verifica JavaScript mediante lint, tests y empaquetado VSIX. -- Dependencias bloqueadas por lockfile y auditoría npm integrada en CI. +### Added + +- SQLite as the fifth database engine, running with `sql.js` in a cancellable Worker. +- Full adapters for SQLite, PostgreSQL, MySQL, SQL Server, and Oracle. +- Multiple profiles, connection testing, connect/disconnect actions, and passwords stored in `SecretStorage`. +- Exploration of databases, schemas, and engine-specific objects: tables, views, materialized views, routines, Oracle packages, indexes, triggers, sequences, types, synonyms, and MySQL events where supported. +- SQL editor sessions with database/schema context. +- Dialect-aware parsing for PostgreSQL `$$`, MySQL `DELIMITER`, SQL Server `GO`, Oracle PL/SQL `/`, and SQLite triggers. +- Selection, current-statement, and document execution for DML, DDL, and arbitrary SQL. +- DDL inspection/templates with actions to show definitions and prepare `CREATE`, `ALTER`, and `DROP` scripts. +- Per-editor transactions, `COMMIT`, `ROLLBACK`, cancellation, and timeouts. +- Disk-backed paged results, multiple result sets, cell/row/selection copy actions, and CSV/JSON/XLSX export. +- Configurable history with duration, retrieved rows, and affected rows. +- Configurable confirmations for destructive operations and DML without `WHERE`. +- Automated tests written entirely in JavaScript, including real SQLite adapter integration. +- SQLite protection against external file changes and active WAL files, plus exact 64-bit integer reads. +- Engine-specific DDL for indexes/triggers and correct removal of constraint-backed indexes when the catalog provides the required information. +- Protection for transaction-start statements written as SQL so PostgreSQL/MySQL remain on a reserved physical connection, including `START TRANSACTION` variants. +- CSV formula-injection protection and exact Oracle `NUMBER` preservation as text. + +### Changed + +- Standardized the README, project documentation, commands, settings, UI messages, generated-template guidance, comments, and test descriptions in English. +- Improved Marketplace discovery with a database-focused display name, expanded search keywords, an English description, and a dedicated Simple DB Marketplace icon. +- Migrated the project completely from TypeScript to JavaScript: no `.ts`, `tsconfig.json`, or TypeScript compilation remains. +- `F5` loads `src/extension.js` directly. +- `simpleDb.maxRows` now defaults to `0`, meaning **no row limit**. Users can configure a limit if needed. +- Result pagination is a storage/display mechanism and does not alter SQL with `TOP`, `LIMIT`, or `FETCH`. +- CI validates JavaScript with linting, tests, and VSIX packaging. +- Dependencies are locked through the lockfile and production dependency auditing is integrated into CI. ## 0.0.1 - 2026-08-05 -### Añadido +### Added -- Esqueleto inicial de la extensión. +- Initial extension skeleton. diff --git a/README.md b/README.md index fa0b205..8d0496b 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,124 @@ # Simple DB -Simple DB `0.1.1` es una extensión de Visual Studio Code, escrita íntegramente en JavaScript, para trabajar con **SQLite, PostgreSQL, MySQL, SQL Server y Oracle** desde una interfaz común. - -La extensión abre documentos SQL normales de VS Code. `F5` inicia directamente `src/extension.js`: no hay TypeScript, `tsconfig.json`, carpeta `dist` ni paso de compilación. - -## Funciones principales - -- Crear, editar, probar y eliminar múltiples conexiones por motor. -- Contraseñas en `SecretStorage`; nunca dentro de los perfiles ni del repositorio. -- Conectar varios motores simultáneamente y desconectarlos de forma explícita. -- Explorar bases de datos, esquemas y los objetos propios de cada motor. -- Abrir editores SQL vinculados a una conexión, base de datos y esquema. -- Ejecutar la selección, la sentencia del cursor o el documento completo. -- Ejecutar SQL libre: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `CREATE`, `ALTER`, `DROP`, `TRUNCATE` y demás sintaxis que acepte el servidor. -- Crear scripts `CREATE`, `ALTER` y `DROP` desde el explorador y consultar la definición/DDL de objetos. -- Transacciones explícitas por editor con `BEGIN`, `COMMIT` y `ROLLBACK`. -- Cancelar consultas y aplicar timeout configurable por conexión. -- Resultados con varias tablas, tipos, `NULL`, filas afectadas, duración y copia de celda/fila/selección. -- Conservación de enteros de 64 bits/`NUMBER` de alta precisión como valores exactos antes de mostrarlos o exportarlos. -- Exportar el resultado ya recuperado a CSV, JSON o XLSX sin volver a ejecutar el SQL. -- Historial local configurable con reapertura, copia y reejecución. - -## Cinco motores - -| Motor | Driver | Dialecto y exploración destacada | +Simple DB `0.1.1` is a Visual Studio Code extension written entirely in JavaScript for working with **SQLite, PostgreSQL, MySQL, SQL Server, and Oracle** through a single interface. + +The extension opens regular VS Code SQL documents. `F5` launches `src/extension.js` directly: there is no TypeScript, `tsconfig.json`, `dist` folder, or compilation step. + +## Main features + +- Create, edit, test, and delete multiple connections for each database engine. +- Store passwords in `SecretStorage`, never inside profiles or the repository. +- Connect to several database engines simultaneously and disconnect them explicitly. +- Explore databases, schemas, and engine-specific objects. +- Open SQL editors linked to a connection, database, and schema. +- Execute the selection, the statement at the cursor, or the entire document. +- Execute arbitrary SQL: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `CREATE`, `ALTER`, `DROP`, `TRUNCATE`, and any other syntax accepted by the server. +- Generate `CREATE`, `ALTER`, and `DROP` scripts from the explorer and inspect object definitions/DDL. +- Use explicit per-editor transactions with `BEGIN`, `COMMIT`, and `ROLLBACK`. +- Cancel queries and configure query timeouts per connection. +- View results with multiple result sets, types, `NULL`, affected rows, duration, and cell/row/selection copy actions. +- Preserve 64-bit integers and high-precision `NUMBER` values exactly before displaying or exporting them. +- Export already-retrieved results to CSV, JSON, or XLSX without executing the SQL again. +- Use configurable local query history with reopen, copy, and rerun actions. + +## Five database engines + +| Engine | Driver | Notable dialect and explorer support | |---|---|---| -| SQLite | `sql.js` en Worker | tablas, vistas, índices, triggers, PRAGMA, transacciones | -| PostgreSQL | `pg` + `pg-cursor` | bases, esquemas, tablas, vistas/materializadas, rutinas, índices, triggers, secuencias, tipos, `$$` | -| MySQL | `mysql2` | bases/esquemas, tablas, vistas, rutinas, índices, triggers, eventos, `DELIMITER` | -| SQL Server | `mssql` | bases, esquemas, tablas, vistas, procedimientos/funciones, índices, triggers, secuencias, tipos, sinónimos, `GO` | -| Oracle | `oracledb` Thin | esquemas, tablas, vistas/materializadas, procedimientos/funciones, packages, índices, triggers, secuencias, tipos, sinónimos, PL/SQL | +| SQLite | `sql.js` in a Worker | tables, views, indexes, triggers, PRAGMA, transactions | +| PostgreSQL | `pg` + `pg-cursor` | databases, schemas, tables, views/materialized views, routines, indexes, triggers, sequences, types, `$$` | +| MySQL | `mysql2` | databases/schemas, tables, views, routines, indexes, triggers, events, `DELIMITER` | +| SQL Server | `mssql` | databases, schemas, tables, views, procedures/functions, indexes, triggers, sequences, types, synonyms, `GO` | +| Oracle | `oracledb` Thin | schemas, tables, views/materialized views, procedures/functions, packages, indexes, triggers, sequences, types, synonyms, PL/SQL | -Oracle utiliza el modo Thin predeterminado de `node-oracledb`, por lo que las conexiones habituales no requieren instalar Oracle Client. SQL Server `0.1.1` utiliza autenticación SQL con usuario y contraseña. +Oracle uses the default `node-oracledb` Thin mode, so normal connections do not require Oracle Client to be installed. Simple DB `0.1.1` uses SQL authentication with a username and password for SQL Server. -## Resultados sin límite impuesto por defecto +## No imposed row limit by default -`simpleDb.maxRows` vale **`0` por defecto: sin límite de filas**. +`simpleDb.maxRows` defaults to **`0`, which means unlimited rows**. -Simple DB no añade automáticamente `TOP`, `LIMIT` ni `FETCH` a una consulta. Los drivers consumen los resultados mediante cursor, result set o streaming y el almacenamiento temporal se divide en páginas para no enviar todas las filas de golpe al webview. +Simple DB does not automatically add `TOP`, `LIMIT`, or `FETCH` to a query. Drivers consume results through cursors, result sets, or streaming, and temporary result storage is divided into pages so the webview does not receive every row at once. -`simpleDb.resultPageSize` (500 por defecto) es únicamente el tamaño de página de almacenamiento/visualización; **no es un límite de filas**. Si el usuario quiere un límite, puede asignar a `simpleDb.maxRows` un valor mayor que cero. +`simpleDb.resultPageSize` (500 by default) controls only the temporary storage/display page size; **it is not a row limit**. If a user wants a limit, `simpleDb.maxRows` can be set to a value greater than zero. -## Explorador y DDL +## Explorer and DDL -Los grupos visibles dependen del motor. Desde un objeto se puede: +Visible object groups depend on the database engine. From an object you can: -- abrir `SELECT *` para tablas, vistas y vistas materializadas, sin límite SQL añadido; -- mostrar su definición cuando el catálogo del servidor la ofrece; -- preparar un script `ALTER`; -- preparar un script `DROP`; -- copiar su nombre cualificado. +- open `SELECT *` for tables, views, and materialized views without adding a SQL row limit; +- show its definition when the server catalog provides one; +- prepare an `ALTER` script; +- prepare a `DROP` script; +- copy its qualified name. -Desde una base, esquema o grupo se puede preparar un `CREATE` del tipo correspondiente. Los scripts se abren primero en un editor: el usuario los revisa y decide si los ejecuta. +From a database, schema, or object group, you can prepare a `CREATE` script for the corresponding object type. Generated scripts open in an editor first so the user can review them and decide whether to execute them. -Los `DROP`/`TRUNCATE` solicitan confirmación por defecto. También se avisa antes de `UPDATE` o `DELETE` sin `WHERE`. Ambos comportamientos son configurables. +`DROP` and `TRUNCATE` request confirmation by default. Simple DB also warns before `UPDATE` or `DELETE` without a `WHERE` clause. Both behaviors are configurable. -## Ejecución por dialecto +## Dialect-aware execution -El documento no se divide con un `split(';')`. El parser entiende: +Documents are not split with a simple `split(';')`. The parser understands: -- PostgreSQL: strings, comentarios y bloques dollar-quoted como `$$ ... $$`; -- MySQL: `DELIMITER`, strings, comentarios `--`, `/* ... */` y `#`; -- SQL Server: batches `GO` y `GO n`; -- Oracle: bloques `DECLARE`/`BEGIN`, procedimientos, funciones, packages, tipos, triggers, terminador `/` y literales `q'[...]'`; -- SQLite: `CREATE TRIGGER ... BEGIN ... END` con sentencias internas. +- PostgreSQL: strings, comments, and dollar-quoted blocks such as `$$ ... $$`; +- MySQL: `DELIMITER`, strings, `--`, `/* ... */`, and `#` comments; +- SQL Server: `GO` and `GO n` batches; +- Oracle: `DECLARE`/`BEGIN` blocks, procedures, functions, packages, types, triggers, `/` terminators, and `q'[...]'` literals; +- SQLite: `CREATE TRIGGER ... BEGIN ... END` with internal statements. -La ejecución de un documento se detiene en el primer error y selecciona el bloque que falló. La selección explícita se procesa dentro de sus límites y los separadores de cliente (`GO`, `DELIMITER`, `/`) no se envían al servidor. +Document execution stops on the first error and selects the failed block. An explicit selection is processed only within its boundaries, and client separators (`GO`, `DELIMITER`, `/`) are not sent to the server. -## Transacciones +## Transactions -Cada editor SQL tiene un identificador de sesión independiente. Una transacción reserva su conexión física hasta `COMMIT` o `ROLLBACK`. +Each SQL editor has an independent session identifier. A transaction reserves its physical connection until `COMMIT` or `ROLLBACK`. -- Cerrar un editor con una transacción activa provoca `ROLLBACK` y muestra un aviso. -- Desconectar una conexión con transacciones abiertas requiere confirmación y realiza `ROLLBACK`. -- Tras un error/cancelación dentro de una transacción, la barra de estado puede exigir `ROLLBACK`. -- SQLite impide que otra pestaña utilice el mismo adaptador mientras un editor tiene una transacción abierta. +- Closing an editor with an active transaction performs `ROLLBACK` and shows a warning. +- Disconnecting a connection with open transactions requires confirmation and performs `ROLLBACK`. +- After an error or cancellation inside a transaction, the status bar can require `ROLLBACK`. +- SQLite prevents another tab from using the same adapter while an editor has an open transaction. -## Resultados, copia y exportación +## Results, copy, and export -El panel de resultados permite navegar por páginas, cambiar entre múltiples result sets, distinguir `NULL`, seleccionar una celda o un rango con `Shift` y copiar celda, fila o selección. +The result panel supports page navigation, switching between multiple result sets, distinguishing `NULL`, selecting a cell or a range with `Shift`, and copying a cell, row, or selection. -CSV, JSON y XLSX se generan en streaming a partir de las páginas temporales recuperadas. No se reejecuta la consulta. Los valores de celda muy grandes se acotan para la vista mediante `simpleDb.maxCellCharacters`; el texto indica explícitamente cuando una celda fue recortada. +CSV, JSON, and XLSX are streamed from the retrieved temporary result pages. The query is not executed again. Very large cell values are bounded for display by `simpleDb.maxCellCharacters`; the UI explicitly indicates when a cell has been truncated for display. -La exportación CSV protege por defecto valores que podrían interpretarse como fórmulas al abrirlos en una hoja de cálculo. Puede desactivarse si se necesita una exportación CSV literal. +CSV export protects values that spreadsheet applications could interpret as formulas by default. This protection can be disabled when literal CSV output is required. -## Conexiones y seguridad +## Connections and security -- Los perfiles no contienen contraseñas. -- Las contraseñas se guardan con la API `SecretStorage` de VS Code. -- SSL/TLS, cifrado y confianza del certificado son opciones explícitas según el motor. -- `simpleDb.confirmDestructiveQueries` está activado por defecto. -- `simpleDb.warnUnsafeDml` está activado por defecto. -- El historial puede contener literales escritos en SQL. Puede desactivarse con `simpleDb.history.enabled` o vaciarse desde la vista **Historial**. +- Connection profiles do not contain passwords. +- Passwords are stored with the VS Code `SecretStorage` API. +- SSL/TLS, encryption, and certificate trust are explicit options where supported by the database engine. +- `simpleDb.confirmDestructiveQueries` is enabled by default. +- `simpleDb.warnUnsafeDml` is enabled by default. +- Query history can contain literals written in SQL. It can be disabled with `simpleDb.history.enabled` or cleared from the **History** view. -### Nota SQLite +### SQLite note -SQLite se ejecuta en un Worker dedicado mediante WebAssembly para que consultas largas no bloqueen la interfaz y puedan cancelarse terminando el Worker. El archivo se mantiene como una instantánea cargada durante la conexión. Antes de cada operación, Simple DB comprueba que el archivo principal/WAL/journal no haya cambiado externamente; ante un conflicto se niega a continuar y pide reconectar. Si al abrir existe un WAL activo, la conexión se rechaza hasta que el proceso propietario haga checkpoint/cierre el WAL, evitando cargar o sobrescribir una instantánea incompleta. +SQLite runs in a dedicated WebAssembly Worker so long-running queries do not block the UI and can be cancelled by terminating the Worker. The database file is held as an in-memory snapshot while connected. Before every operation, Simple DB checks whether the main file, WAL, or journal changed externally. If a conflict is detected, it refuses to continue and asks the user to reconnect. If an active WAL exists when the connection is opened, the connection is rejected until the owning process checkpoints/closes the WAL, preventing Simple DB from loading or overwriting an incomplete snapshot. -## Configuración +## Configuration -| Ajuste | Predeterminado | Función | +| Setting | Default | Purpose | |---|---:|---| -| `simpleDb.maxRows` | `0` | Límite opcional por result set; `0` = ilimitado | -| `simpleDb.resultPageSize` | `500` | Filas por página temporal/visual | -| `simpleDb.maxCellCharacters` | `10000` | Máximo conservado por celda en resultados | -| `simpleDb.history.enabled` | `true` | Guardar historial local | -| `simpleDb.history.maxEntries` | `500` | Entradas máximas de historial | -| `simpleDb.confirmDestructiveQueries` | `true` | Confirmar `DROP`/`TRUNCATE` | -| `simpleDb.warnUnsafeDml` | `true` | Avisar de `UPDATE`/`DELETE` sin `WHERE` | -| `simpleDb.csvDelimiter` | `;` | Delimitador de exportación CSV | -| `simpleDb.csvProtectFormulaInjection` | `true` | Neutralizar posibles fórmulas al exportar CSV | +| `simpleDb.maxRows` | `0` | Optional limit per result set; `0` = unlimited | +| `simpleDb.resultPageSize` | `500` | Rows per temporary storage/display page | +| `simpleDb.maxCellCharacters` | `10000` | Maximum characters retained per cell in results | +| `simpleDb.history.enabled` | `true` | Store local query history | +| `simpleDb.history.maxEntries` | `500` | Maximum history entries | +| `simpleDb.confirmDestructiveQueries` | `true` | Confirm `DROP`/`TRUNCATE` | +| `simpleDb.warnUnsafeDml` | `true` | Warn about `UPDATE`/`DELETE` without `WHERE` | +| `simpleDb.csvDelimiter` | `;` | CSV export delimiter | +| `simpleDb.csvProtectFormulaInjection` | `true` | Neutralize potential spreadsheet formulas in CSV export | -El timeout de conexión y el timeout máximo de consulta se configuran por perfil. `0` en el timeout de consulta significa sin timeout. +Connection timeout and maximum query timeout are configured per connection profile. A query timeout of `0` means no timeout. -## Desarrollo +## Development -Requisitos: +Requirements: -- Visual Studio Code `1.95.0` o posterior. -- Node.js `20` o posterior para desarrollo. +- Visual Studio Code `1.95.0` or later. +- Node.js `20` or later for development. - npm. ```bash @@ -126,37 +126,37 @@ npm ci npm run check ``` -Después abre el repositorio en VS Code y pulsa `F5` con la configuración **Ejecutar Simple DB**. El `preLaunchTask` ejecuta ESLint y el Extension Host carga `src/extension.js` directamente. +Then open the repository in VS Code and press `F5` with the **Run Simple DB** launch configuration. The `preLaunchTask` runs ESLint and the Extension Host loads `src/extension.js` directly. -Comandos del proyecto: +Project commands: -| Comando | Función | +| Command | Purpose | |---|---| -| `npm run lint` | ESLint sobre JavaScript | -| `npm test` | Pruebas Vitest escritas en JavaScript | -| `npm run check` | Lint + tests | -| `npm run package` | Verificación y creación del VSIX | -| `npm run package:win32` | VSIX objetivo `win32-x64` | -| `npm run package:linux` | VSIX objetivo `linux-x64` | +| `npm run lint` | Run ESLint on JavaScript sources | +| `npm test` | Run JavaScript Vitest tests | +| `npm run check` | Run lint + tests | +| `npm run package` | Validate and build the VSIX | +| `npm run package:win32` | Build a `win32-x64` VSIX | +| `npm run package:linux` | Build a `linux-x64` VSIX | -Las pruebas incluyen parser/safety/DDL/almacenamiento/SecretStorage simulado y una integración SQLite real. Los servidores PostgreSQL, MySQL, SQL Server y Oracle externos requieren sus credenciales/infraestructura para pruebas de integración contra una instancia real. +The test suite covers parsing, safety rules, DDL, storage, mocked `SecretStorage`, and a real SQLite integration. External PostgreSQL, MySQL, SQL Server, and Oracle servers require their own credentials/infrastructure for integration testing against live instances. -## Estructura +## Project structure ```text src/ - adapters/ # cinco adaptadores y Worker SQLite - core/ # errores y normalización de valores - managers/ # conexiones y sesiones de editor - services/ # ejecución y exportación - sql/ # splitter por dialecto, safety y DDL - storage/ # perfiles, historial y resultados paginados - test/ # tests JavaScript - ui/ # formulario de conexión - views/ # árboles y panel de resultados + adapters/ # five adapters and the SQLite Worker + core/ # errors and value normalization + managers/ # connections and editor sessions + services/ # execution and export + sql/ # dialect-aware splitter, safety rules, and DDL + storage/ # profiles, history, and paged results + test/ # JavaScript tests + ui/ # connection form + views/ # trees and result panel extension.js # activate/deactivate ``` -## Licencia +## License -MIT. Consulta `LICENSE`. +MIT. See `LICENSE`. diff --git a/package.json b/package.json index f13ea1d..d116a5e 100644 --- a/package.json +++ b/package.json @@ -61,77 +61,77 @@ "commands": [ { "command": "simpleDb.addConnection", - "title": "Añadir conexión", + "title": "Add Connection", "category": "Simple DB", "icon": "$(add)" }, { "command": "simpleDb.refreshConnections", - "title": "Actualizar conexiones", + "title": "Refresh Connections", "category": "Simple DB", "icon": "$(refresh)" }, { "command": "simpleDb.connect", - "title": "Conectar", + "title": "Connect", "category": "Simple DB", "icon": "$(plug)" }, { "command": "simpleDb.disconnect", - "title": "Desconectar", + "title": "Disconnect", "category": "Simple DB", "icon": "$(debug-disconnect)" }, { "command": "simpleDb.testConnection", - "title": "Probar conexión", + "title": "Test Connection", "category": "Simple DB", "icon": "$(beaker)" }, { "command": "simpleDb.editConnection", - "title": "Editar conexión", + "title": "Edit Connection", "category": "Simple DB", "icon": "$(edit)" }, { "command": "simpleDb.deleteConnection", - "title": "Eliminar conexión", + "title": "Delete Connection", "category": "Simple DB", "icon": "$(trash)" }, { "command": "simpleDb.newQuery", - "title": "Nueva consulta", + "title": "New Query", "category": "Simple DB", "icon": "$(new-file)" }, { "command": "simpleDb.changeEditorConnection", - "title": "Cambiar conexión del editor", + "title": "Change Editor Connection", "category": "Simple DB" }, { "command": "simpleDb.executeCurrent", - "title": "Ejecutar selección o sentencia actual", + "title": "Execute Selection or Current Statement", "category": "Simple DB", "icon": "$(play)" }, { "command": "simpleDb.executeSelection", - "title": "Ejecutar selección", + "title": "Execute Selection", "category": "Simple DB" }, { "command": "simpleDb.executeDocument", - "title": "Ejecutar documento completo", + "title": "Execute Entire Document", "category": "Simple DB", "icon": "$(run-all)" }, { "command": "simpleDb.cancelQuery", - "title": "Cancelar consulta", + "title": "Cancel Query", "category": "Simple DB", "icon": "$(debug-stop)" }, @@ -152,69 +152,69 @@ }, { "command": "simpleDb.showHistory", - "title": "Mostrar historial", + "title": "Show History", "category": "Simple DB", "icon": "$(history)" }, { "command": "simpleDb.clearHistory", - "title": "Vaciar historial", + "title": "Clear History", "category": "Simple DB", "icon": "$(clear-all)" }, { "command": "simpleDb.openHistoryEntry", - "title": "Abrir consulta del historial", + "title": "Open History Query", "category": "Simple DB" }, { "command": "simpleDb.copyHistoryEntry", - "title": "Copiar SQL", + "title": "Copy SQL", "category": "Simple DB" }, { "command": "simpleDb.rerunHistoryEntry", - "title": "Volver a ejecutar", + "title": "Run Again", "category": "Simple DB" }, { "command": "simpleDb.deleteHistoryEntry", - "title": "Eliminar del historial", + "title": "Delete from History", "category": "Simple DB" }, { "command": "simpleDb.selectTable", - "title": "Abrir SELECT *", + "title": "Open SELECT *", "category": "Simple DB", "icon": "$(preview)" }, { "command": "simpleDb.showDefinition", - "title": "Mostrar definición / DDL", + "title": "Show Definition / DDL", "category": "Simple DB", "icon": "$(code)" }, { "command": "simpleDb.createObject", - "title": "Crear objeto SQL", + "title": "Create SQL Object", "category": "Simple DB", "icon": "$(add)" }, { "command": "simpleDb.alterObject", - "title": "Preparar ALTER", + "title": "Prepare ALTER", "category": "Simple DB", "icon": "$(edit)" }, { "command": "simpleDb.dropObjectScript", - "title": "Preparar DROP", + "title": "Prepare DROP", "category": "Simple DB", "icon": "$(trash)" }, { "command": "simpleDb.copyQualifiedName", - "title": "Copiar nombre cualificado", + "title": "Copy Qualified Name", "category": "Simple DB", "icon": "$(copy)" } @@ -232,12 +232,12 @@ "simpleDb": [ { "id": "simpleDb.connections", - "name": "Conexiones", + "name": "Connections", "type": "tree" }, { "id": "simpleDb.history", - "name": "Historial", + "name": "History", "type": "tree" } ] @@ -409,53 +409,53 @@ "default": 0, "minimum": 0, "maximum": 100000000, - "description": "Máximo opcional de filas recuperadas por conjunto de resultados. 0 = sin límite. Simple DB no modifica el SQL con TOP/LIMIT/FETCH." + "description": "Optional maximum number of rows retrieved per result set. 0 = unlimited. Simple DB does not modify SQL with TOP/LIMIT/FETCH." }, "simpleDb.resultPageSize": { "type": "integer", "default": 500, "minimum": 50, "maximum": 5000, - "description": "Filas almacenadas y mostradas por página." + "description": "Rows stored and displayed per page." }, "simpleDb.maxCellCharacters": { "type": "integer", "default": 10000, "minimum": 100, "maximum": 1000000, - "description": "Máximo de caracteres conservados por celda en la vista de resultados." + "description": "Maximum number of characters retained per cell in the result view." }, "simpleDb.history.enabled": { "type": "boolean", "default": true, - "description": "Guardar historial local de consultas." + "description": "Store local query history." }, "simpleDb.history.maxEntries": { "type": "integer", "default": 500, "minimum": 0, "maximum": 5000, - "description": "Número máximo de consultas conservadas en el historial." + "description": "Maximum number of queries retained in history." }, "simpleDb.confirmDestructiveQueries": { "type": "boolean", "default": true, - "description": "Pedir confirmación antes de DROP o TRUNCATE." + "description": "Ask for confirmation before DROP or TRUNCATE." }, "simpleDb.warnUnsafeDml": { "type": "boolean", "default": true, - "description": "Avisar antes de UPDATE o DELETE sin cláusula WHERE." + "description": "Warn before UPDATE or DELETE without a WHERE clause." }, "simpleDb.csvDelimiter": { "type": "string", "default": ";", - "description": "Separador utilizado al exportar resultados a CSV." + "description": "Delimiter used when exporting results to CSV." }, "simpleDb.csvProtectFormulaInjection": { "type": "boolean", "default": true, - "description": "Protege celdas CSV que comienzan por =, +, -, @, tabulador o retorno de carro para evitar que una hoja de cálculo las interprete como fórmulas." + "description": "Protect CSV cells that begin with =, +, -, @, tab, or carriage return so spreadsheet applications do not interpret them as formulas." } } } diff --git a/plan.txt b/plan.txt index 67660dd..ac29347 100644 --- a/plan.txt +++ b/plan.txt @@ -1,57 +1,57 @@ -SIMPLE DB 0.1.1 — PLAN DE ENTREGA +SIMPLE DB 0.1.1 — DELIVERY PLAN -1. Arquitectura JavaScript - [x] Eliminar TypeScript, tsconfig y compilación. - [x] Activación directa desde src/extension.js. - [x] Estructura nativa adapters/core/managers/services/sql/storage/ui/views/test. +1. JavaScript architecture + [x] Remove TypeScript, tsconfig, and compilation. + [x] Activate directly from src/extension.js. + [x] Native adapters/core/managers/services/sql/storage/ui/views/test structure. -2. Cinco motores +2. Five database engines [x] SQLite. [x] PostgreSQL. [x] MySQL. [x] SQL Server. [x] Oracle. -3. Conexiones - [x] Crear/editar/probar/eliminar perfiles. - [x] SecretStorage para contraseñas. - [x] Pools/conexiones simultáneas, estados y desconexión segura. - [x] Contexto de base de datos/esquema por editor. - -4. Explorador - [x] Bases/esquemas/tablas/vistas/columnas/rutinas. - [x] Vistas materializadas, índices, triggers, secuencias y tipos. - [x] Packages Oracle, sinónimos y eventos donde corresponda. - [x] Definición de objetos y nombres cualificados. - -5. SQL y DDL - [x] Documento/selección/sentencia actual. - [x] PostgreSQL $$, MySQL DELIMITER/#, SQL Server GO, Oracle PL/SQL/q-quotes, triggers SQLite. - [x] DML/DDL libre. - [x] Plantillas CREATE/ALTER/DROP y lectura DDL. - [x] Confirmación de DROP/TRUNCATE y aviso UPDATE/DELETE sin WHERE. - -6. Transacciones/cancelación - [x] BEGIN/COMMIT/ROLLBACK por editor. - [x] Conexión reservada por transacción. - [x] Cancelación PostgreSQL/MySQL/SQL Server/Oracle/SQLite. - [x] Timeout y estado de rollback requerido. - -7. Resultados - [x] Streaming/cursor/result set por driver. - [x] maxRows=0 sin límite por defecto. - [x] Paginación temporal sin TOP/LIMIT/FETCH añadido. - [x] Múltiples result sets, NULL, tipos, tiempos y filas. - [x] Copiar celda/fila/selección. - [x] Exportación CSV/JSON/XLSX. - [x] Historial configurable. - -8. Calidad y entrega - [x] ESLint JavaScript. - [x] Tests JavaScript, incluida integración SQLite. - [x] Validación final npm run check (50 tests). - [x] Auditoría npm sin vulnerabilidades. - [x] Empaquetado VSIX validado como ZIP. - [x] Comprobar que no queda ningún archivo/configuración TypeScript en fuente ni VSIX. - [x] Commit final de version-0.1.1. - [x] Publicar rama version-0.1.1 en GitHub. +3. Connections + [x] Create/edit/test/delete profiles. + [x] SecretStorage for passwords. + [x] Simultaneous pools/connections, connection states, and safe disconnect. + [x] Per-editor database/schema context. + +4. Explorer + [x] Databases/schemas/tables/views/columns/routines. + [x] Materialized views, indexes, triggers, sequences, and types. + [x] Oracle packages, synonyms, and events where supported. + [x] Object definitions and qualified names. + +5. SQL and DDL + [x] Document/selection/current-statement execution. + [x] PostgreSQL $$, MySQL DELIMITER/#, SQL Server GO, Oracle PL/SQL/q-quotes, SQLite triggers. + [x] Arbitrary DML/DDL. + [x] CREATE/ALTER/DROP templates and DDL inspection. + [x] DROP/TRUNCATE confirmation and UPDATE/DELETE-without-WHERE warning. + +6. Transactions/cancellation + [x] Per-editor BEGIN/COMMIT/ROLLBACK. + [x] Physical connection reserved for each transaction. + [x] PostgreSQL/MySQL/SQL Server/Oracle/SQLite cancellation. + [x] Timeout and rollback-required state. + +7. Results + [x] Driver streaming/cursor/result-set support. + [x] maxRows=0 with no limit by default. + [x] Temporary paging without added TOP/LIMIT/FETCH. + [x] Multiple result sets, NULL, types, timings, and row counts. + [x] Copy cell/row/selection. + [x] CSV/JSON/XLSX export. + [x] Configurable history. + +8. Quality and delivery + [x] JavaScript ESLint. + [x] JavaScript tests, including SQLite integration. + [x] Final npm run check validation (50 tests). + [x] npm audit with no vulnerabilities. + [x] VSIX packaging validated as a ZIP archive. + [x] Verify that no TypeScript source/configuration remains in the source tree or VSIX. + [x] Final version-0.1.1 commit. + [x] Publish version-0.1.1 branch to GitHub. diff --git a/project requirements.txt b/project requirements.txt index 4340764..43ff0ea 100644 --- a/project requirements.txt +++ b/project requirements.txt @@ -1,62 +1,62 @@ -SIMPLE DB 0.1.1 — REQUISITOS DE PRODUCTO +SIMPLE DB 0.1.1 — PRODUCT REQUIREMENTS -Lenguaje y ejecución -- Todo el código de la extensión y todos los tests se escriben en JavaScript. -- No se usa TypeScript, tsconfig.json ni un paso de compilación TypeScript. -- F5 carga directamente src/extension.js. +Language and execution +- All extension code and tests are written in JavaScript. +- TypeScript, tsconfig.json, and a TypeScript compilation step are not used. +- F5 loads src/extension.js directly. -Motores obligatorios +Required database engines - SQLite. - PostgreSQL. - MySQL. - SQL Server. - Oracle. -Conexiones -- Crear, editar, probar y eliminar conexiones. -- Varias conexiones por motor y varios motores conectados a la vez. -- Contraseñas en VS Code SecretStorage. -- SSL/TLS/cifrado y timeouts según el motor. - -Exploración -- Bases de datos y esquemas cuando el motor los exponga. -- Tablas, vistas y columnas. -- Vistas materializadas donde existan. -- Procedimientos y funciones. -- Packages Oracle. -- Índices, triggers, secuencias y tipos cuando existan. -- Sinónimos en Oracle/SQL Server y eventos MySQL. -- Mostrar definición/DDL y preparar CREATE, ALTER y DROP. - -Editor y ejecución -- Documento SQL normal de VS Code vinculado a perfil/base/esquema. -- Ejecutar documento completo, selección o sentencia del cursor. -- SELECT, INSERT, UPDATE, DELETE, MERGE y cualquier DML soportado por el motor. -- CREATE, ALTER, DROP, TRUNCATE y demás DDL soportado por el motor. -- PostgreSQL dollar quotes, MySQL DELIMITER, SQL Server GO, Oracle PL/SQL y terminador /, triggers SQLite. -- Detener documento en el primer error y seleccionar el bloque fallido. - -Transacciones y cancelación -- Auto-commit fuera de una transacción explícita. -- BEGIN, COMMIT y ROLLBACK por editor. -- Conexión física reservada durante una transacción. -- ROLLBACK al cerrar editor/desconectar con transacción abierta. -- Cancelación por mecanismo nativo del driver. -- Timeout configurable por perfil. - -Resultados -- Tabla, tipos, NULL, varias tablas de resultados, filas recuperadas/afectadas y duración. -- Copiar celda, fila y selección. -- Exportar CSV, JSON y XLSX sin reejecutar la consulta. -- Historial local configurable. - -Política de filas -- simpleDb.maxRows = 0 por defecto: SIN LÍMITE. -- El usuario puede elegir un límite mayor que cero si lo necesita. -- simpleDb.resultPageSize controla páginas de almacenamiento/visualización, no el número total de filas. -- Simple DB NO inyecta TOP, LIMIT ni FETCH en el SQL del usuario. - -Protecciones -- Confirmación configurable para DROP/TRUNCATE. -- Aviso configurable para UPDATE/DELETE sin WHERE. -- Los scripts destructivos generados se abren en el editor; no se ejecutan automáticamente. +Connections +- Create, edit, test, and delete connections. +- Support multiple connections per engine and multiple connected engines at the same time. +- Store passwords in VS Code SecretStorage. +- Support SSL/TLS/encryption and timeouts as appropriate for each engine. + +Exploration +- Databases and schemas where exposed by the engine. +- Tables, views, and columns. +- Materialized views where available. +- Procedures and functions. +- Oracle packages. +- Indexes, triggers, sequences, and types where available. +- Synonyms in Oracle/SQL Server and MySQL events. +- Show definitions/DDL and prepare CREATE, ALTER, and DROP scripts. + +Editor and execution +- Regular VS Code SQL documents linked to a profile/database/schema. +- Execute the entire document, a selection, or the statement at the cursor. +- SELECT, INSERT, UPDATE, DELETE, MERGE, and any DML supported by the engine. +- CREATE, ALTER, DROP, TRUNCATE, and any other DDL supported by the engine. +- PostgreSQL dollar quotes, MySQL DELIMITER, SQL Server GO, Oracle PL/SQL and `/` terminator, SQLite triggers. +- Stop document execution at the first error and select the failed block. + +Transactions and cancellation +- Auto-commit outside an explicit transaction. +- Per-editor BEGIN, COMMIT, and ROLLBACK. +- Reserve the physical connection for the duration of a transaction. +- ROLLBACK when closing an editor/disconnecting with an open transaction. +- Cancellation through the driver's native mechanism. +- Configurable timeout per profile. + +Results +- Table output, types, NULL, multiple result sets, retrieved/affected rows, and duration. +- Copy a cell, row, or selection. +- Export CSV, JSON, and XLSX without executing the query again. +- Configurable local history. + +Row policy +- simpleDb.maxRows = 0 by default: NO LIMIT. +- Users can choose a limit greater than zero when needed. +- simpleDb.resultPageSize controls storage/display pages, not the total number of rows. +- Simple DB DOES NOT inject TOP, LIMIT, or FETCH into user SQL. + +Protections +- Configurable confirmation for DROP/TRUNCATE. +- Configurable warning for UPDATE/DELETE without WHERE. +- Generated destructive scripts open in the editor and are never executed automatically. diff --git a/src/adapters/baseAdapter.js b/src/adapters/baseAdapter.js index dfca9af..b4ae06d 100644 --- a/src/adapters/baseAdapter.js +++ b/src/adapters/baseAdapter.js @@ -31,7 +31,7 @@ class BaseAdapter { try { await this.rollback(sessionId); } catch (_error) { - // El cierre posterior del pool/conexión libera también la transacción. + // Closing the pool/connection later also releases the transaction. } } } @@ -41,7 +41,7 @@ class BaseAdapter { try { await this.cancel(executionId); } catch (_error) { - // Se continúa con la limpieza del resto de recursos. + // Continue cleaning up the remaining resources. } } } diff --git a/src/adapters/factory.js b/src/adapters/factory.js index 8e9b31f..4ff632c 100644 --- a/src/adapters/factory.js +++ b/src/adapters/factory.js @@ -19,7 +19,7 @@ function createAdapter(profile, password) { case 'oracle': return new OracleAdapter(profile, password); default: - throw new Error(`Motor no soportado: ${profile.engine}`); + throw new Error(`Unsupported database engine: ${profile.engine}`); } } diff --git a/src/adapters/mysqlAdapter.js b/src/adapters/mysqlAdapter.js index 4b63858..bf801ab 100644 --- a/src/adapters/mysqlAdapter.js +++ b/src/adapters/mysqlAdapter.js @@ -107,7 +107,7 @@ class MySqlAdapter extends BaseAdapter { async begin(sessionId, context = {}) { if (this.transactions.has(sessionId)) { - throw new Error('Esta sesión ya tiene una transacción MySQL activa.'); + throw new Error('This session already has an active MySQL transaction.'); } const connection = await this._getConnection(); try { @@ -127,7 +127,7 @@ class MySqlAdapter extends BaseAdapter { async commit(sessionId) { const connection = this.transactions.get(sessionId); if (!connection) { - throw new Error('No hay una transacción activa en esta sesión.'); + throw new Error('There is no active transaction in this session.'); } try { await callbackPromise((done) => connection.commit(done)); diff --git a/src/adapters/oracleAdapter.js b/src/adapters/oracleAdapter.js index e02d368..c6007de 100644 --- a/src/adapters/oracleAdapter.js +++ b/src/adapters/oracleAdapter.js @@ -5,8 +5,8 @@ const { BaseAdapter } = require('./baseAdapter'); const { normalizeDatabaseError } = require('../core/errors'); const { looksLikeRowQuery } = require('../sql/safety'); -// NUMBER como string evita perder precisión al cruzar el límite seguro de -// enteros de JavaScript; ResultStore conserva después el valor textual exacto. +// Returning NUMBER as a string avoids precision loss beyond JavaScript's safe +// integer range; ResultStore then preserves the exact textual value. const stringFetchTypes = [ oracledb.DB_TYPE_NUMBER, oracledb.DB_TYPE_CLOB, @@ -103,7 +103,7 @@ class OracleAdapter extends BaseAdapter { async begin(sessionId, context = {}) { if (this.transactions.has(sessionId)) { - throw new Error('Esta sesión ya tiene una transacción Oracle activa.'); + throw new Error('This session already has an active Oracle transaction.'); } let connection; try { @@ -119,7 +119,7 @@ class OracleAdapter extends BaseAdapter { async commit(sessionId) { const connection = this.transactions.get(sessionId); if (!connection) { - throw new Error('No hay una transacción activa en esta sesión.'); + throw new Error('There is no active transaction in this session.'); } try { await connection.commit(); diff --git a/src/adapters/postgresqlAdapter.js b/src/adapters/postgresqlAdapter.js index d80b4e6..de30898 100644 --- a/src/adapters/postgresqlAdapter.js +++ b/src/adapters/postgresqlAdapter.js @@ -90,7 +90,7 @@ class PostgreSqlAdapter extends BaseAdapter { async begin(sessionId, context = {}) { if (this.transactions.has(sessionId)) { - throw new Error('Esta sesión ya tiene una transacción PostgreSQL activa.'); + throw new Error('This session already has an active PostgreSQL transaction.'); } const acquired = await this._acquireClient(context.database); try { @@ -107,7 +107,7 @@ class PostgreSqlAdapter extends BaseAdapter { async commit(sessionId) { const transaction = this.transactions.get(sessionId); if (!transaction) { - throw new Error('No hay una transacción activa en esta sesión.'); + throw new Error('There is no active transaction in this session.'); } try { await transaction.client.query('COMMIT'); diff --git a/src/adapters/sqlServerAdapter.js b/src/adapters/sqlServerAdapter.js index 99e517a..375fe70 100644 --- a/src/adapters/sqlServerAdapter.js +++ b/src/adapters/sqlServerAdapter.js @@ -130,7 +130,7 @@ class SqlServerAdapter extends BaseAdapter { async begin(sessionId, context = {}) { if (this.transactions.has(sessionId)) { - throw new Error('Esta sesión ya tiene una transacción SQL Server activa.'); + throw new Error('This session already has an active SQL Server transaction.'); } try { const pool = await this._poolFor(context.database); @@ -145,7 +145,7 @@ class SqlServerAdapter extends BaseAdapter { async commit(sessionId) { const transaction = this.transactions.get(sessionId); if (!transaction) { - throw new Error('No hay una transacción activa en esta sesión.'); + throw new Error('There is no active transaction in this session.'); } try { await transaction.commit(); @@ -232,7 +232,7 @@ class SqlServerAdapter extends BaseAdapter { } }); - // El evento error necesita listener incluso usando la promesa de query(). + // The error event needs a listener even when using the query() promise. request.on('error', () => {}); this.activeExecutions.set(options.executionId, { request }); diff --git a/src/adapters/sqliteAdapter.js b/src/adapters/sqliteAdapter.js index 537ee87..111db0a 100644 --- a/src/adapters/sqliteAdapter.js +++ b/src/adapters/sqliteAdapter.js @@ -27,7 +27,7 @@ class SqliteAdapter extends BaseAdapter { this.connected = false; } if (code !== 0) { - this._rejectWorkerPending(worker, new Error(`SQLite worker finalizó con código ${code}.`)); + this._rejectWorkerPending(worker, new Error(`SQLite worker exited with code ${code}.`)); } }); return worker; @@ -99,7 +99,7 @@ class SqliteAdapter extends BaseAdapter { _request(type, payload = {}, options = {}) { if (!this.worker) { - return Promise.reject(new Error('SQLite no está conectado.')); + return Promise.reject(new Error('SQLite is not connected.')); } const requestId = options.requestId || randomUUID(); const worker = this.worker; @@ -162,11 +162,11 @@ class SqliteAdapter extends BaseAdapter { async begin(sessionId) { if (this.transactions.size > 0 && !this.transactions.has(sessionId)) { throw new Error( - 'SQLite admite una única transacción activa por archivo en Simple DB.', + 'Simple DB allows only one active SQLite transaction per file.', ); } if (this.transactions.has(sessionId)) { - throw new Error('Esta sesión ya tiene una transacción SQLite activa.'); + throw new Error('This session already has an active SQLite transaction.'); } await this._request('begin'); this.transactions.set(sessionId, true); @@ -174,7 +174,7 @@ class SqliteAdapter extends BaseAdapter { async commit(sessionId) { if (!this.transactions.has(sessionId)) { - throw new Error('No hay una transacción activa en esta sesión.'); + throw new Error('There is no active transaction in this session.'); } await this._request('commit'); this.transactions.delete(sessionId); @@ -191,7 +191,7 @@ class SqliteAdapter extends BaseAdapter { async execute(sessionId, sql, options) { if (this.transactions.size > 0 && !this.transactions.has(sessionId)) { throw new Error( - 'SQLite tiene una transacción activa en otro editor. Finaliza esa transacción antes de ejecutar aquí.', + 'SQLite has an active transaction in another editor. Finish that transaction before executing here.', ); } const requestId = randomUUID(); diff --git a/src/adapters/sqliteWorker.js b/src/adapters/sqliteWorker.js index 1edc801..e4a5143 100644 --- a/src/adapters/sqliteWorker.js +++ b/src/adapters/sqliteWorker.js @@ -113,7 +113,7 @@ function applyTransactionControl(control, wasInTransaction) { function assertWritableSql(sql) { if (readOnly && databaseChangedBy(sql)) { - const error = new Error('La conexión SQLite está configurada como solo lectura.'); + const error = new Error('The SQLite connection is configured as read-only.'); error.code = 'SQLITE_READONLY'; throw error; } @@ -141,7 +141,7 @@ function assertNoActiveWal() { try { if (fs.statSync(`${filePath}-wal`).size > 0) { const error = new Error( - 'La base SQLite tiene un archivo WAL activo. Para evitar leer una instantánea incompleta, haz CHECKPOINT/cierra el proceso escritor y vuelve a conectar.', + 'The SQLite database has an active WAL file. To avoid reading an incomplete snapshot, CHECKPOINT/close the writer process and reconnect.', ); error.code = 'SQLITE_BUSY_WAL'; throw error; @@ -156,7 +156,7 @@ function assertDiskUnchanged() { const current = readDiskFingerprint(); if (current !== diskFingerprint) { const error = new Error( - 'El archivo SQLite fue modificado por otra aplicación desde que se abrió. Simple DB no lo sobrescribirá; desconecta y vuelve a conectar para recargarlo.', + 'The SQLite file was modified by another application after it was opened. Simple DB will not overwrite it; disconnect and reconnect to reload it.', ); error.code = 'SQLITE_BUSY_EXTERNAL'; throw error; @@ -179,7 +179,7 @@ function persistDatabase() { try { fs.unlinkSync(temporaryPath); } catch (_unlinkError) { - // El archivo temporal se limpiará en la próxima escritura si sigue presente. + // The temporary file will be cleaned up on the next write if it still exists. } } diskFingerprint = readDiskFingerprint(); @@ -201,7 +201,7 @@ async function openDatabase(payload) { readOnly = Boolean(payload.readOnly); if (readOnly && !fs.existsSync(filePath)) { - const error = new Error(`No existe el archivo SQLite: ${filePath}`); + const error = new Error(`SQLite file does not exist: ${filePath}`); error.code = 'SQLITE_CANTOPEN'; throw error; } @@ -231,7 +231,7 @@ async function openDatabase(payload) { function queryAll(sql) { if (!database) { - throw new Error('SQLite no está conectado.'); + throw new Error('SQLite is not connected.'); } assertDiskUnchanged(); const results = database.exec(sql, { useBigInt: true }); @@ -266,10 +266,10 @@ function sendStreamEvent(requestId, event, payload) { async function executeSql(requestId, payload) { if (!database) { - throw new Error('SQLite no está conectado.'); + throw new Error('SQLite is not connected.'); } - // sql.js trabaja sobre un snapshot en memoria. Si otro proceso modifica el - // archivo (o crea WAL/journal), se obliga a reconectar también para lecturas. + // sql.js works on an in-memory snapshot. If another process modifies the file + // (or creates a WAL/journal), reconnecting is required before reads as well. assertDiskUnchanged(); assertWritableSql(payload.sql); @@ -381,7 +381,7 @@ async function handleCommand(message) { return executeSql(message.requestId, message.payload); case 'begin': if (inTransaction) { - throw new Error('Ya existe una transacción SQLite activa.'); + throw new Error('An active SQLite transaction already exists.'); } assertDiskUnchanged(); database.run('BEGIN TRANSACTION'); @@ -392,7 +392,7 @@ async function handleCommand(message) { return {}; case 'commit': if (!inTransaction) { - throw new Error('No hay una transacción SQLite activa.'); + throw new Error('There is no active SQLite transaction.'); } if (transactionDirty) assertDiskUnchanged(); database.run('COMMIT'); @@ -425,7 +425,7 @@ async function handleCommand(message) { } return {}; default: - throw new Error(`Comando SQLite desconocido: ${message.type}`); + throw new Error(`Unknown SQLite command: ${message.type}`); } } diff --git a/src/core/errors.js b/src/core/errors.js index ea41453..0e551c5 100644 --- a/src/core/errors.js +++ b/src/core/errors.js @@ -1,7 +1,7 @@ 'use strict'; class QueryCancelledError extends Error { - constructor(message = 'Consulta cancelada por el usuario.') { + constructor(message = 'Query cancelled by the user.') { super(message); this.name = 'QueryCancelledError'; this.code = 'SIMPLE_DB_CANCELLED'; diff --git a/src/extension.js b/src/extension.js index 9d1e3b9..fa72399 100644 --- a/src/extension.js +++ b/src/extension.js @@ -44,10 +44,10 @@ function profileForNode(connectionStore, node) { return node?.profileId ? connectionStore.get(node.profileId) : null; } -async function pickProfile(connectionStore, title = 'Simple DB — Seleccionar conexión') { +async function pickProfile(connectionStore, title = 'Simple DB — Select Connection') { const profiles = connectionStore.list(); if (!profiles.length) { - throw new Error('No hay conexiones configuradas. Crea una conexión primero.'); + throw new Error('No connections are configured. Create a connection first.'); } const pick = await vscode.window.showQuickPick( profiles.map((profile) => ({ @@ -180,12 +180,12 @@ async function activate(context) { form.effectivePassword, ); vscode.window.showInformationMessage( - `Simple DB: conexión correcta (${result.elapsedMs} ms) · ${result.serverVersion}`, + `Simple DB: connection successful (${result.elapsedMs} ms) · ${result.serverVersion}`, ); } const saved = await connectionStore.save(form.profile, form.password); connectionsProvider.refresh(); - vscode.window.showInformationMessage(`Simple DB: conexión "${saved.name}" guardada.`); + vscode.window.showInformationMessage(`Simple DB: connection "${saved.name}" saved.`); }); registerCommand(context, 'simpleDb.refreshConnections', () => { @@ -194,27 +194,27 @@ async function activate(context) { registerCommand(context, 'simpleDb.connect', async (node) => { const profile = profileForNode(connectionStore, node); - if (!profile) throw new Error('Conexión no encontrada.'); + if (!profile) throw new Error('Connection not found.'); const adapter = await connectionManager.connect(profile.id); vscode.window.showInformationMessage( - `Simple DB: conectado a ${profile.name} · ${adapter.serverVersion}`, + `Simple DB: connected to ${profile.name} · ${adapter.serverVersion}`, ); }); registerCommand(context, 'simpleDb.disconnect', async (node) => { const profile = profileForNode(connectionStore, node); - if (!profile) throw new Error('Conexión no encontrada.'); + if (!profile) throw new Error('Connection not found.'); const transactionCount = connectionManager.transactionCount(profile.id); const executionCount = connectionManager.executionCount(profile.id); if (transactionCount > 0 || executionCount > 0) { const details = [ - executionCount > 0 ? `${executionCount} consulta(s) activa(s)` : '', - transactionCount > 0 ? `${transactionCount} transacción(es) activa(s)` : '', - ].filter(Boolean).join(' y '); + executionCount > 0 ? `${executionCount} active query/queries` : '', + transactionCount > 0 ? `${transactionCount} active transaction(s)` : '', + ].filter(Boolean).join(' and '); const answer = await vscode.window.showWarningMessage( - `Hay ${details}. Desconectar cancelará las consultas y hará ROLLBACK de las transacciones.`, + `There are ${details}. Disconnecting will cancel queries and ROLLBACK transactions.`, { modal: true }, - 'Cancelar, hacer ROLLBACK y desconectar', + 'Cancel queries, ROLLBACK, and disconnect', ); if (!answer) return; } @@ -223,16 +223,16 @@ async function activate(context) { registerCommand(context, 'simpleDb.testConnection', async (node) => { const profile = profileForNode(connectionStore, node); - if (!profile) throw new Error('Conexión no encontrada.'); + if (!profile) throw new Error('Connection not found.'); const result = await connectionManager.testConnection(profile.id); vscode.window.showInformationMessage( - `Simple DB: ${profile.name} responde en ${result.elapsedMs} ms · ${result.serverVersion}`, + `Simple DB: ${profile.name} responded in ${result.elapsedMs} ms · ${result.serverVersion}`, ); }); registerCommand(context, 'simpleDb.editConnection', async (node) => { const profile = profileForNode(connectionStore, node); - if (!profile) throw new Error('Conexión no encontrada.'); + if (!profile) throw new Error('Connection not found.'); const existingPassword = await connectionStore.getPassword(profile.id); const form = await promptConnection({ existingProfile: profile, @@ -246,7 +246,7 @@ async function activate(context) { form.effectivePassword, ); vscode.window.showInformationMessage( - `Simple DB: parámetros verificados en ${result.elapsedMs} ms · ${result.serverVersion}`, + `Simple DB: settings verified in ${result.elapsedMs} ms · ${result.serverVersion}`, ); } @@ -254,14 +254,14 @@ async function activate(context) { const transactions = connectionManager.transactionCount(profile.id); const executions = connectionManager.executionCount(profile.id); const text = transactions > 0 || executions > 0 - ? `Guardar requiere desconectar: se cancelarán ${executions} consulta(s) y se hará ROLLBACK de ${transactions} transacción(es).` - : 'Guardar requiere desconectar la conexión activa.'; + ? `Saving requires disconnecting: ${executions} query/queries will be cancelled and ${transactions} transaction(s) will be rolled back.` + : 'Saving requires disconnecting the active connection.'; const answer = await vscode.window.showWarningMessage( text, { modal: true }, transactions > 0 || executions > 0 - ? 'Guardar, cancelar y hacer ROLLBACK' - : 'Guardar y desconectar', + ? 'Save, cancel queries, and ROLLBACK' + : 'Save and disconnect', ); if (!answer) return; await connectionManager.disconnect(profile.id); @@ -274,18 +274,18 @@ async function activate(context) { registerCommand(context, 'simpleDb.deleteConnection', async (node) => { const profile = profileForNode(connectionStore, node); - if (!profile) throw new Error('Conexión no encontrada.'); + if (!profile) throw new Error('Connection not found.'); const transactions = connectionManager.transactionCount(profile.id); const executions = connectionManager.executionCount(profile.id); const warning = transactions > 0 || executions > 0 - ? ` Se cancelarán ${executions} consulta(s) y se hará ROLLBACK de ${transactions} transacción(es) activa(s).` + ? ` ${executions} query/queries will be cancelled and ${transactions} active transaction(s) will be rolled back.` : ''; const answer = await vscode.window.showWarningMessage( - `¿Eliminar la conexión "${profile.name}"?${warning}`, + `Delete connection "${profile.name}"?${warning}`, { modal: true }, - 'Eliminar', + 'Delete', ); - if (answer !== 'Eliminar') return; + if (answer !== 'Delete') return; await connectionManager.disconnect(profile.id); await connectionStore.delete(profile.id); connectionsProvider.refresh(); @@ -312,7 +312,7 @@ async function activate(context) { registerCommand(context, 'simpleDb.cancelQuery', async () => { const session = editorSessionManager.getActive(); if (!session?.runningExecutionId) { - vscode.window.showInformationMessage('Simple DB: no hay una consulta activa que cancelar.'); + vscode.window.showInformationMessage('Simple DB: there is no active query to cancel.'); return; } await connectionManager.cancel(session.profileId, session.runningExecutionId); @@ -322,47 +322,47 @@ async function activate(context) { const session = await editorSessionManager.ensureActiveSession(); if (!session) return; if (session.runningExecutionId) { - throw new Error('Espera o cancela la consulta activa antes de iniciar una transacción.'); + throw new Error('Wait for or cancel the active query before starting a transaction.'); } if (connectionManager.hasTransaction(session.profileId, session.id)) { - throw new Error('Ya existe una transacción activa en este editor.'); + throw new Error('This editor already has an active transaction.'); } await connectionManager.begin(session.profileId, session.id, { database: session.database, schema: session.schema, }); editorSessionManager.markTransactionNeedsRollback(session, false); - vscode.window.showInformationMessage('Simple DB: transacción iniciada.'); + vscode.window.showInformationMessage('Simple DB: transaction started.'); }); registerCommand(context, 'simpleDb.commit', async () => { const session = await editorSessionManager.ensureActiveSession(); if (!session) return; if (session.runningExecutionId) { - throw new Error('Espera o cancela la consulta activa antes de hacer COMMIT.'); + throw new Error('Wait for or cancel the active query before COMMIT.'); } if (session.transactionNeedsRollback) { - throw new Error('La transacción tuvo un error. Ejecuta ROLLBACK antes de continuar.'); + throw new Error('The transaction encountered an error. Run ROLLBACK before continuing.'); } await connectionManager.commit(session.profileId, session.id); editorSessionManager.markTransactionNeedsRollback(session, false); - vscode.window.showInformationMessage('Simple DB: COMMIT completado.'); + vscode.window.showInformationMessage('Simple DB: COMMIT completed.'); }); registerCommand(context, 'simpleDb.rollback', async () => { const session = await editorSessionManager.ensureActiveSession(); if (!session) return; if (session.runningExecutionId) { - throw new Error('Cancela o espera a que termine la consulta antes de hacer ROLLBACK.'); + throw new Error('Cancel or wait for the active query before ROLLBACK.'); } await connectionManager.rollback(session.profileId, session.id); editorSessionManager.markTransactionNeedsRollback(session, false); - vscode.window.showInformationMessage('Simple DB: ROLLBACK completado.'); + vscode.window.showInformationMessage('Simple DB: ROLLBACK completed.'); }); registerCommand(context, 'simpleDb.selectTable', async (node) => { if (node?.kind !== 'object' || !['table', 'view', 'materializedView'].includes(node.objectType)) { - throw new Error('Selecciona una tabla o vista del explorador.'); + throw new Error('Select a table or view in the explorer.'); } const profile = profileForNode(connectionStore, node); await connectionManager.ensureConnected(profile.id); @@ -379,7 +379,7 @@ async function activate(context) { }); registerCommand(context, 'simpleDb.showDefinition', async (node) => { - if (node?.kind !== 'object') throw new Error('Selecciona un objeto de base de datos.'); + if (node?.kind !== 'object') throw new Error('Select a database object.'); const profile = profileForNode(connectionStore, node); await connectionManager.ensureConnected(profile.id); const definition = await connectionManager.getObjectDefinition( @@ -392,7 +392,7 @@ async function activate(context) { ); if (!definition) { vscode.window.showWarningMessage( - `Simple DB: el servidor no devolvió la definición de ${node.name}.`, + `Simple DB: the server did not return a definition for ${node.name}.`, ); return; } @@ -413,7 +413,7 @@ async function activate(context) { const types = objectTypesForEngine(profile.engine); const pick = await vscode.window.showQuickPick( types.map((type) => ({ label: OBJECT_LABELS[type] || type, type })), - { title: `Simple DB — Crear objeto en ${profile.name}` }, + { title: `Simple DB — Create Object in ${profile.name}` }, ); objectType = pick?.type; } else if (node?.groupType === 'procedures') { @@ -422,20 +422,20 @@ async function activate(context) { if (!objectType && node?.groupType === 'procedures') { const routine = await vscode.window.showQuickPick( [ - { label: 'Procedimiento', type: 'procedure' }, - { label: 'Función', type: 'function' }, + { label: 'Procedure', type: 'procedure' }, + { label: 'Function', type: 'function' }, ], - { title: `Simple DB — Crear rutina en ${profile.name}` }, + { title: `Simple DB — Create Routine in ${profile.name}` }, ); objectType = routine?.type; } if (!objectType) return; const name = await vscode.window.showInputBox({ - title: `Simple DB — Crear ${OBJECT_LABELS[objectType] || objectType}`, - prompt: 'Nombre del nuevo objeto', + title: `Simple DB — Create ${OBJECT_LABELS[objectType] || objectType}`, + prompt: 'New object name', ignoreFocusOut: true, - validateInput: (value) => (value.trim() ? null : 'El nombre es obligatorio.'), + validateInput: (value) => (value.trim() ? null : 'Name is required.'), }); if (!name) return; const qualified = ddlQualifiedName( @@ -458,7 +458,7 @@ async function activate(context) { }); registerCommand(context, 'simpleDb.alterObject', async (node) => { - if (node?.kind !== 'object') throw new Error('Selecciona un objeto de base de datos.'); + if (node?.kind !== 'object') throw new Error('Select a database object.'); const profile = profileForNode(connectionStore, node); await connectionManager.ensureConnected(profile.id); const objectType = effectiveObjectType(node); @@ -496,7 +496,7 @@ async function activate(context) { }); registerCommand(context, 'simpleDb.dropObjectScript', async (node) => { - if (node?.kind !== 'object') throw new Error('Selecciona un objeto de base de datos.'); + if (node?.kind !== 'object') throw new Error('Select a database object.'); const profile = profileForNode(connectionStore, node); await connectionManager.ensureConnected(profile.id); const objectType = effectiveObjectType(node); @@ -539,7 +539,7 @@ async function activate(context) { }); registerCommand(context, 'simpleDb.copyQualifiedName', async (node) => { - if (node?.kind !== 'object') throw new Error('Selecciona un objeto de base de datos.'); + if (node?.kind !== 'object') throw new Error('Select a database object.'); const profile = profileForNode(connectionStore, node); await connectionManager.ensureConnected(profile.id); const qualified = connectionManager.quoteTable( @@ -558,20 +558,20 @@ async function activate(context) { registerCommand(context, 'simpleDb.clearHistory', async () => { if (!historyStore.list().length) return; const answer = await vscode.window.showWarningMessage( - '¿Vaciar todo el historial local de consultas de Simple DB?', + 'Clear all local Simple DB query history?', { modal: true }, - 'Vaciar historial', + 'Clear History', ); - if (answer === 'Vaciar historial') await historyStore.clear(); + if (answer === 'Clear History') await historyStore.clear(); }); const historyEntry = (node) => node?.entry || historyStore.get(node?.id); registerCommand(context, 'simpleDb.openHistoryEntry', async (node) => { const entry = historyEntry(node); - if (!entry) throw new Error('La entrada de historial ya no existe.'); + if (!entry) throw new Error('The history entry no longer exists.'); const profile = connectionStore.get(entry.profileId); - if (!profile) throw new Error('La conexión asociada a esta consulta ya no existe.'); + if (!profile) throw new Error('The connection associated with this query no longer exists.'); await editorSessionManager.createQuery(profile, entry.sql, { database: entry.database, schema: entry.schema, @@ -580,15 +580,15 @@ async function activate(context) { registerCommand(context, 'simpleDb.copyHistoryEntry', async (node) => { const entry = historyEntry(node); - if (!entry) throw new Error('La entrada de historial ya no existe.'); + if (!entry) throw new Error('The history entry no longer exists.'); await vscode.env.clipboard.writeText(entry.sql); }); registerCommand(context, 'simpleDb.rerunHistoryEntry', async (node) => { const entry = historyEntry(node); - if (!entry) throw new Error('La entrada de historial ya no existe.'); + if (!entry) throw new Error('The history entry no longer exists.'); const profile = connectionStore.get(entry.profileId); - if (!profile) throw new Error('La conexión asociada a esta consulta ya no existe.'); + if (!profile) throw new Error('The connection associated with this query no longer exists.'); await editorSessionManager.createQuery(profile, entry.sql, { database: entry.database, schema: entry.schema, @@ -617,12 +617,12 @@ async function activate(context) { } if (hadTransaction) { vscode.window.showWarningMessage( - 'Simple DB: se hizo ROLLBACK de la transacción al cerrar el editor SQL.', + 'Simple DB: the transaction was rolled back when the SQL editor closed.', ); } } catch (error) { vscode.window.showErrorMessage( - `Simple DB: no se pudo cerrar la transacción del editor: ${error.message}`, + `Simple DB: the editor transaction could not be closed: ${error.message}`, ); } }); diff --git a/src/managers/connectionManager.js b/src/managers/connectionManager.js index e408b6f..fb0f6bd 100644 --- a/src/managers/connectionManager.js +++ b/src/managers/connectionManager.js @@ -58,7 +58,7 @@ class ConnectionManager extends EventEmitter { async _connect(profileId) { const profile = this.connectionStore.get(profileId); if (!profile) { - throw new Error('La conexión ya no existe.'); + throw new Error('The connection no longer exists.'); } this._setStatus(profileId, 'connecting'); const password = await this.connectionStore.getPassword(profileId); @@ -109,7 +109,7 @@ class ConnectionManager extends EventEmitter { async testConnection(profileId) { const profile = this.connectionStore.get(profileId); if (!profile) { - throw new Error('La conexión ya no existe.'); + throw new Error('The connection no longer exists.'); } const password = await this.connectionStore.getPassword(profileId); return this.testProfile(profile, password); @@ -124,7 +124,7 @@ class ConnectionManager extends EventEmitter { getAdapter(profileId) { const adapter = this.adapters.get(profileId); if (!adapter?.isConnected()) { - throw new Error('La conexión no está conectada.'); + throw new Error('The connection is not connected.'); } return adapter; } diff --git a/src/managers/editorSessionManager.js b/src/managers/editorSessionManager.js index 760f9c2..6adf31d 100644 --- a/src/managers/editorSessionManager.js +++ b/src/managers/editorSessionManager.js @@ -14,7 +14,7 @@ class EditorSessionManager { 100, ); this.statusBar.command = 'simpleDb.changeEditorConnection'; - this.statusBar.tooltip = 'Simple DB: cambiar la conexión del editor SQL'; + this.statusBar.tooltip = 'Simple DB: change the SQL editor connection'; this.activeEditorDisposable = vscode.window.onDidChangeActiveTextEditor(() => { this.refreshStatusBar(); @@ -79,7 +79,7 @@ class EditorSessionManager { async ensureActiveSession() { const editor = vscode.window.activeTextEditor; if (!editor) { - throw new Error('No hay un editor SQL activo.'); + throw new Error('There is no active SQL editor.'); } const existing = this.get(editor.document); if (existing) { @@ -88,7 +88,7 @@ class EditorSessionManager { const profiles = this.connectionStore.list(); if (!profiles.length) { - throw new Error('Primero debes crear una conexión en Simple DB.'); + throw new Error('Create a connection in Simple DB first.'); } const pick = await vscode.window.showQuickPick( profiles.map((profile) => ({ @@ -97,8 +97,8 @@ class EditorSessionManager { profile, })), { - title: 'Simple DB — Vincular editor a una conexión', - placeHolder: 'Selecciona la conexión para este editor SQL', + title: 'Simple DB — Link Editor to Connection', + placeHolder: 'Select the connection for this SQL editor', }, ); if (!pick) { @@ -114,10 +114,10 @@ class EditorSessionManager { } const session = this.get(editor.document); if (session?.runningExecutionId) { - throw new Error('Cancela o espera a que termine la consulta antes de cambiar de conexión.'); + throw new Error('Cancel or wait for the active query before changing connections.'); } if (session && this.connectionManager.hasTransaction(session.profileId, session.id)) { - throw new Error('Haz COMMIT o ROLLBACK antes de cambiar la conexión del editor.'); + throw new Error('Run COMMIT or ROLLBACK before changing the editor connection.'); } const profiles = this.connectionStore.list(); @@ -127,7 +127,7 @@ class EditorSessionManager { description: getDatabaseEngine(profile.engine)?.displayName || profile.engine, profile, })), - { title: 'Simple DB — Cambiar conexión del editor' }, + { title: 'Simple DB — Change Editor Connection' }, ); if (!pick) { return; @@ -165,7 +165,7 @@ class EditorSessionManager { } const profile = this.connectionStore.get(session.profileId); if (!profile) { - this.statusBar.text = '$(warning) Simple DB | conexión eliminada'; + this.statusBar.text = '$(warning) Simple DB | connection deleted'; this.statusBar.show(); return; } @@ -174,11 +174,11 @@ class EditorSessionManager { const connected = this.connectionManager.isConnected(profile.id); const transaction = this.connectionManager.hasTransaction(profile.id, session.id); const mode = session.transactionNeedsRollback - ? 'TX requiere ROLLBACK' + ? 'TX requires ROLLBACK' : transaction - ? 'TX activa' + ? 'TX active' : 'Auto-commit'; - const running = session.runningExecutionId ? ' | $(sync~spin) Ejecutando' : ''; + const running = session.runningExecutionId ? ' | $(sync~spin) Running' : ''; const connectionIcon = connected ? '$(database)' : '$(circle-slash)'; this.statusBar.text = `${connectionIcon} ${engine} | ${profile.name} | ${session.database || '-'} | ${mode}${running}`; this.statusBar.show(); diff --git a/src/services/exportService.js b/src/services/exportService.js index 748aab9..74b5582 100644 --- a/src/services/exportService.js +++ b/src/services/exportService.js @@ -49,13 +49,13 @@ class ExportService { const metadata = this.resultStore.getMetadata(executionId); const set = metadata?.sets?.[setIndex]; if (!set || set.kind !== 'rows') { - throw new Error('El conjunto de resultados ya no está disponible para exportar.'); + throw new Error('The result set is no longer available for export.'); } const extension = format === 'xlsx' ? 'xlsx' : format; const defaultName = `simple-db-${new Date().toISOString().replaceAll(':', '-')}.${extension}`; const uri = await vscode.window.showSaveDialog({ - title: `Exportar resultado como ${format.toUpperCase()}`, + title: `Export Result as ${format.toUpperCase()}`, defaultUri: vscode.Uri.file(path.join(process.cwd(), defaultName)), filters: { [format.toUpperCase()]: [extension], @@ -70,7 +70,7 @@ class ExportService { } else if (format === 'xlsx') { await this._writeXlsx(uri.fsPath, executionId, setIndex, set); } else { - throw new Error(`Formato de exportación no soportado: ${format}`); + throw new Error(`Unsupported export format: ${format}`); } return uri.fsPath; } @@ -130,7 +130,7 @@ class ExportService { useStyles: true, useSharedStrings: false, }); - const sheet = workbook.addWorksheet('Resultado'); + const sheet = workbook.addWorksheet('Result'); const header = sheet.addRow(set.columns.map((column) => column.name)); header.font = { bold: true }; header.commit(); diff --git a/src/services/queryRunner.js b/src/services/queryRunner.js index 6a55617..574ca69 100644 --- a/src/services/queryRunner.js +++ b/src/services/queryRunner.js @@ -36,7 +36,7 @@ class QueryRunner { const selection = editor.selection; if (mode === 'selection') { if (selection.isEmpty) { - throw new Error('Selecciona primero el SQL que quieres ejecutar.'); + throw new Error('Select the SQL you want to execute first.'); } const selected = editor.document.getText(selection); return { @@ -76,17 +76,17 @@ class QueryRunner { const safety = classifySqlSafety(statement.sql, engineId); let warning = ''; if (safety.destructive && config.confirmDestructiveQueries) { - warning = `${safety.operation} puede eliminar o truncar objetos/datos. ¿Ejecutar?`; + warning = `${safety.operation} can delete or truncate objects/data. Execute it?`; } else if (safety.unsafeDml && config.warnUnsafeDml) { - warning = `${safety.operation} no contiene cláusula WHERE. Puede afectar a todas las filas. ¿Ejecutar?`; + warning = `${safety.operation} has no WHERE clause. It may affect every row. Execute it?`; } if (!warning) continue; const answer = await vscode.window.showWarningMessage( warning, { modal: true }, - 'Ejecutar', + 'Execute', ); - if (answer !== 'Ejecutar') return false; + if (answer !== 'Execute') return false; } return true; } @@ -133,20 +133,20 @@ class QueryRunner { async run(mode = 'current') { const editor = vscode.window.activeTextEditor; if (!editor) { - throw new Error('No hay un editor SQL activo.'); + throw new Error('There is no active SQL editor.'); } const session = await this.editorSessionManager.ensureActiveSession(); if (!session) return null; if (session.runningExecutionId) { - throw new Error('Ya hay una consulta en ejecución en este editor. Cancélala o espera a que termine.'); + throw new Error('A query is already running in this editor. Cancel it or wait for it to finish.'); } const profile = this.connectionStore.get(session.profileId); - if (!profile) throw new Error('La conexión vinculada al editor ya no existe.'); + if (!profile) throw new Error('The connection linked to this editor no longer exists.'); const extracted = this._statements(editor, profile.engine, mode); if (extracted.statements.length === 0) { - throw new Error('No se ha encontrado ninguna sentencia SQL ejecutable.'); + throw new Error('No executable SQL statement was found.'); } const config = this._configuration(); if (!(await this._confirmSafety(extracted.statements, profile.engine, config))) { @@ -202,7 +202,7 @@ class QueryRunner { const statement = extracted.statements[index]; currentStatement = statement; progress.report({ - message: `sentencia ${index + 1}/${extracted.statements.length}`, + message: `statement ${index + 1}/${extracted.statements.length}`, }); const safety = classifySqlSafety(statement.sql, profile.engine); @@ -234,7 +234,7 @@ class QueryRunner { statementIndex: index, sql: statement.sql, durationMs: Date.now() - statementStarted, - message: 'BEGIN completado. Transacción activa en este editor.', + message: 'BEGIN completed. Transaction active in this editor.', }); continue; } @@ -247,7 +247,7 @@ class QueryRunner { session.transactionNeedsRollback ) { throw new Error( - 'La transacción tuvo un error. Ejecuta ROLLBACK antes de COMMIT.', + 'The transaction encountered an error. Run ROLLBACK before COMMIT.', ); } await this.connectionManager[transactionControl]( @@ -259,7 +259,7 @@ class QueryRunner { statementIndex: index, sql: statement.sql, durationMs: Date.now() - statementStarted, - message: `${transactionControl.toUpperCase()} completado.`, + message: `${transactionControl.toUpperCase()} completed.`, }); continue; } @@ -296,13 +296,13 @@ class QueryRunner { sql: statement.sql, affectedRows, durationMs: Date.now() - statementStarted, - message: `${result.command || safety.operation || 'SQL'} ejecutado correctamente${affectedRows ? ` · ${affectedRows} filas afectadas` : ''}.`, + message: `${result.command || safety.operation || 'SQL'} executed successfully${affectedRows ? ` · ${affectedRows} rows affected` : ''}.`, }); } } catch (error) { if (timedOut) { const timeoutError = new Error( - `La consulta superó el tiempo máximo configurado (${queryTimeoutMs} ms).`, + `The query exceeded the configured timeout (${queryTimeoutMs} ms).`, ); timeoutError.code = 'SIMPLE_DB_TIMEOUT'; throw timeoutError; @@ -336,7 +336,7 @@ class QueryRunner { } await this.resultStore.addMessageSet(executionId, nextGlobalSet(), { kind: cancelled ? 'message' : 'error', - message: cancelled ? 'Consulta cancelada.' : `Error: ${error.message}`, + message: cancelled ? 'Query cancelled.' : `Error: ${error.message}`, }); } finally { this.editorSessionManager.setRunning(session, null); @@ -380,7 +380,7 @@ class QueryRunner { await this.resultPanel.show(executionId); if (!failure) { vscode.window.setStatusBarMessage( - `$(check) Simple DB: ${totalRows} filas${totalAffectedRows ? ` · ${totalAffectedRows} afectadas` : ''} · ${durationMs} ms`, + `$(check) Simple DB: ${totalRows} rows${totalAffectedRows ? ` · ${totalAffectedRows} affected` : ''} · ${durationMs} ms`, 5000, ); } else if (!cancelled) { diff --git a/src/sql/ddlTemplates.js b/src/sql/ddlTemplates.js index 12f9a23..6fdf022 100644 --- a/src/sql/ddlTemplates.js +++ b/src/sql/ddlTemplates.js @@ -41,18 +41,18 @@ const OBJECT_TYPES_BY_ENGINE = Object.freeze({ }); const OBJECT_LABELS = Object.freeze({ - table: 'Tabla', - view: 'Vista', - materializedView: 'Vista materializada', - procedure: 'Procedimiento', - function: 'Función', + table: 'Table', + view: 'View', + materializedView: 'Materialized View', + procedure: 'Procedure', + function: 'Function', package: 'Package', - index: 'Índice', + index: 'Index', trigger: 'Trigger', - sequence: 'Secuencia', - type: 'Tipo', - synonym: 'Sinónimo', - event: 'Evento', + sequence: 'Sequence', + type: 'Type', + synonym: 'Synonym', + event: 'Event', }); function objectTypesForEngine(engineId) { @@ -67,8 +67,8 @@ function createTemplate( options = {}, ) { const q = quoteIdentifier; - const tablePlaceholder = q('tabla'); - const columnPlaceholder = q('columna'); + const tablePlaceholder = q('table'); + const columnPlaceholder = q('column'); if (objectType === 'table') { const identity = { @@ -79,7 +79,7 @@ function createTemplate( sqlite: 'INTEGER PRIMARY KEY', }[engineId] || 'INTEGER PRIMARY KEY'; const textType = engineId === 'oracle' ? 'VARCHAR2(255)' : 'VARCHAR(255)'; - return `CREATE TABLE ${qualifiedName} (\n ${q('id')} ${identity},\n ${q('nombre')} ${textType} NOT NULL\n);`; + return `CREATE TABLE ${qualifiedName} (\n ${q('id')} ${identity},\n ${q('name')} ${textType} NOT NULL\n);`; } if (objectType === 'view') { @@ -96,8 +96,8 @@ function createTemplate( } if (objectType === 'index') { - // PostgreSQL, MySQL y SQL Server determinan el esquema del índice por la - // tabla y no aceptan un nombre de índice cualificado en CREATE INDEX. + // PostgreSQL, MySQL, and SQL Server derive the index schema from the table + // and do not accept a schema-qualified index name in CREATE INDEX. const indexName = ['postgresql', 'mysql', 'sqlserver'].includes(engineId) ? options.identifierName || qualifiedName : qualifiedName; @@ -115,18 +115,18 @@ function createTemplate( const isFunction = objectType === 'function'; if (engineId === 'postgresql') { return isFunction - ? `CREATE OR REPLACE FUNCTION ${qualifiedName}()\nRETURNS void\nLANGUAGE plpgsql\nAS $$\nBEGIN\n -- implementación\nEND;\n$$;` - : `CREATE OR REPLACE PROCEDURE ${qualifiedName}()\nLANGUAGE plpgsql\nAS $$\nBEGIN\n -- implementación\nEND;\n$$;`; + ? `CREATE OR REPLACE FUNCTION ${qualifiedName}()\nRETURNS void\nLANGUAGE plpgsql\nAS $$\nBEGIN\n -- implementation\nEND;\n$$;` + : `CREATE OR REPLACE PROCEDURE ${qualifiedName}()\nLANGUAGE plpgsql\nAS $$\nBEGIN\n -- implementation\nEND;\n$$;`; } if (engineId === 'mysql') { return isFunction ? `DELIMITER $$\nCREATE FUNCTION ${qualifiedName}()\nRETURNS INT\nDETERMINISTIC\nBEGIN\n RETURN 0;\nEND$$\nDELIMITER ;` - : `DELIMITER $$\nCREATE PROCEDURE ${qualifiedName}()\nBEGIN\n -- implementación\nEND$$\nDELIMITER ;`; + : `DELIMITER $$\nCREATE PROCEDURE ${qualifiedName}()\nBEGIN\n -- implementation\nEND$$\nDELIMITER ;`; } if (engineId === 'sqlserver') { return isFunction ? `CREATE OR ALTER FUNCTION ${qualifiedName}()\nRETURNS INT\nAS\nBEGIN\n RETURN 0;\nEND;` - : `CREATE OR ALTER PROCEDURE ${qualifiedName}\nAS\nBEGIN\n SET NOCOUNT ON;\n -- implementación\nEND;`; + : `CREATE OR ALTER PROCEDURE ${qualifiedName}\nAS\nBEGIN\n SET NOCOUNT ON;\n -- implementation\nEND;`; } if (engineId === 'oracle') { return isFunction @@ -136,22 +136,22 @@ function createTemplate( } if (objectType === 'package' && engineId === 'oracle') { - return `CREATE OR REPLACE PACKAGE ${qualifiedName} AS\n PROCEDURE ejemplo;\nEND;\n/\n\nCREATE OR REPLACE PACKAGE BODY ${qualifiedName} AS\n PROCEDURE ejemplo AS\n BEGIN\n NULL;\n END ejemplo;\nEND;\n/`; + return `CREATE OR REPLACE PACKAGE ${qualifiedName} AS\n PROCEDURE example;\nEND;\n/\n\nCREATE OR REPLACE PACKAGE BODY ${qualifiedName} AS\n PROCEDURE example AS\n BEGIN\n NULL;\n END example;\nEND;\n/`; } if (objectType === 'trigger') { if (engineId === 'sqlite') { - return `CREATE TRIGGER ${qualifiedName}\nAFTER INSERT ON ${tablePlaceholder}\nBEGIN\n -- sentencia SQL;\n SELECT 1;\nEND;`; + return `CREATE TRIGGER ${qualifiedName}\nAFTER INSERT ON ${tablePlaceholder}\nBEGIN\n -- SQL statement;\n SELECT 1;\nEND;`; } if (engineId === 'postgresql') { const triggerName = options.identifierName || qualifiedName; - return `-- PostgreSQL requiere una función de trigger existente.\nCREATE TRIGGER ${triggerName}\nBEFORE INSERT ON ${tablePlaceholder}\nFOR EACH ROW\nEXECUTE FUNCTION ${q('funcion_trigger')}();`; + return `-- PostgreSQL requires an existing trigger function.\nCREATE TRIGGER ${triggerName}\nBEFORE INSERT ON ${tablePlaceholder}\nFOR EACH ROW\nEXECUTE FUNCTION ${q('trigger_function')}();`; } if (engineId === 'mysql') { - return `DELIMITER $$\nCREATE TRIGGER ${qualifiedName}\nBEFORE INSERT ON ${tablePlaceholder}\nFOR EACH ROW\nBEGIN\n -- implementación\nEND$$\nDELIMITER ;`; + return `DELIMITER $$\nCREATE TRIGGER ${qualifiedName}\nBEFORE INSERT ON ${tablePlaceholder}\nFOR EACH ROW\nBEGIN\n -- implementation\nEND$$\nDELIMITER ;`; } if (engineId === 'sqlserver') { - return `CREATE OR ALTER TRIGGER ${qualifiedName}\nON ${tablePlaceholder}\nAFTER INSERT\nAS\nBEGIN\n SET NOCOUNT ON;\n -- implementación\nEND;`; + return `CREATE OR ALTER TRIGGER ${qualifiedName}\nON ${tablePlaceholder}\nAFTER INSERT\nAS\nBEGIN\n SET NOCOUNT ON;\n -- implementation\nEND;`; } if (engineId === 'oracle') { return `CREATE OR REPLACE TRIGGER ${qualifiedName}\nBEFORE INSERT ON ${tablePlaceholder}\nFOR EACH ROW\nBEGIN\n NULL;\nEND;\n/`; @@ -160,13 +160,13 @@ function createTemplate( if (objectType === 'type') { if (engineId === 'postgresql') { - return `CREATE TYPE ${qualifiedName} AS (\n ${q('valor')} TEXT\n);`; + return `CREATE TYPE ${qualifiedName} AS (\n ${q('value')} TEXT\n);`; } if (engineId === 'sqlserver') { - return `CREATE TYPE ${qualifiedName} AS TABLE (\n ${q('valor')} NVARCHAR(255) NOT NULL\n);`; + return `CREATE TYPE ${qualifiedName} AS TABLE (\n ${q('value')} NVARCHAR(255) NOT NULL\n);`; } if (engineId === 'oracle') { - return `CREATE OR REPLACE TYPE ${qualifiedName} AS OBJECT (\n ${q('valor')} VARCHAR2(255)\n);\n/`; + return `CREATE OR REPLACE TYPE ${qualifiedName} AS OBJECT (\n ${q('value')} VARCHAR2(255)\n);\n/`; } } @@ -175,10 +175,10 @@ function createTemplate( } if (objectType === 'event' && engineId === 'mysql') { - return `DELIMITER $$\nCREATE EVENT ${qualifiedName}\nON SCHEDULE EVERY 1 DAY\nDO\nBEGIN\n -- implementación\nEND$$\nDELIMITER ;`; + return `DELIMITER $$\nCREATE EVENT ${qualifiedName}\nON SCHEDULE EVERY 1 DAY\nDO\nBEGIN\n -- implementation\nEND$$\nDELIMITER ;`; } - throw new Error(`No hay plantilla CREATE para ${engineId}/${objectType}.`); + throw new Error(`No CREATE template is available for ${engineId}/${objectType}.`); } function alterTemplate(engineId, objectType, qualifiedName, options = {}) { @@ -195,19 +195,19 @@ function alterTemplate(engineId, objectType, qualifiedName, options = {}) { } if (engineId === 'mysql') return `ALTER VIEW ${qualifiedName} AS\n-- SELECT ...;`; if (engineId === 'sqlite') { - return `-- SQLite no implementa ALTER VIEW. Conserva la definición, elimina la vista y créala de nuevo.\n-- DROP VIEW ${qualifiedName};\n-- CREATE VIEW ${qualifiedName} AS SELECT ...;`; + return `-- SQLite does not implement ALTER VIEW. Keep the definition, drop the view, and create it again.\n-- DROP VIEW ${qualifiedName};\n-- CREATE VIEW ${qualifiedName} AS SELECT ...;`; } } if (objectType === 'materializedView') { if (engineId === 'oracle') { return `ALTER MATERIALIZED VIEW ${qualifiedName}\n -- COMPILE;`; } - return `ALTER MATERIALIZED VIEW ${qualifiedName}\n -- RENAME TO nuevo_nombre;`; + return `ALTER MATERIALIZED VIEW ${qualifiedName}\n -- RENAME TO new_name;`; } if (objectType === 'procedure' || objectType === 'function') { const keyword = objectType.toUpperCase(); if (engineId === 'postgresql') return `ALTER ${keyword} ${qualifiedName}()\n -- ...;`; - if (engineId === 'sqlserver') return `CREATE OR ALTER ${keyword} ${qualifiedName}\nAS\n-- implementación`; + if (engineId === 'sqlserver') return `CREATE OR ALTER ${keyword} ${qualifiedName}\nAS\n-- implementation`; if (engineId === 'oracle') return `ALTER ${keyword} ${qualifiedName} COMPILE;`; if (engineId === 'mysql') return `ALTER ${keyword} ${qualifiedName}\n -- COMMENT '...';`; } @@ -220,18 +220,18 @@ function alterTemplate(engineId, objectType, qualifiedName, options = {}) { if (objectType === 'event' && engineId === 'mysql') return `ALTER EVENT ${qualifiedName}\n -- ...;`; if (objectType === 'type') { if (engineId === 'postgresql') { - return `ALTER TYPE ${qualifiedName}\n -- ADD VALUE 'nuevo_valor';`; + return `ALTER TYPE ${qualifiedName}\n -- ADD VALUE 'new_value';`; } if (engineId === 'oracle') { return `ALTER TYPE ${qualifiedName} COMPILE;`; } if (engineId === 'sqlserver') { - return `-- SQL Server no ofrece ALTER TYPE para cambiar la definición de un tipo.\n-- Revisa dependencias y utiliza DROP TYPE + CREATE TYPE para ${qualifiedName}.`; + return `-- SQL Server does not support ALTER TYPE for changing a type definition.\n-- Review dependencies and use DROP TYPE + CREATE TYPE for ${qualifiedName}.`; } } if (objectType === 'index') { if (engineId === 'postgresql') { - return `ALTER INDEX ${qualifiedName}\n -- RENAME TO nuevo_nombre;`; + return `ALTER INDEX ${qualifiedName}\n -- RENAME TO new_name;`; } if (engineId === 'oracle') { return `ALTER INDEX ${qualifiedName} REBUILD;`; @@ -240,27 +240,27 @@ function alterTemplate(engineId, objectType, qualifiedName, options = {}) { return `ALTER INDEX ${identifierName} ON ${tableQualifiedName}\n REBUILD;`; } if (engineId === 'mysql' && tableQualifiedName) { - return `-- MySQL 8+: cambia visibilidad; otros cambios se hacen con ALTER TABLE.\nALTER TABLE ${tableQualifiedName}\n ALTER INDEX ${identifierName} VISIBLE;`; + return `-- MySQL 8+: change visibility; other changes are made with ALTER TABLE.\nALTER TABLE ${tableQualifiedName}\n ALTER INDEX ${identifierName} VISIBLE;`; } - return `-- Este motor no ofrece un ALTER INDEX genérico seguro para ${qualifiedName}.\n-- Consulta la definición y utiliza la operación específica del motor.`; + return `-- This engine does not provide a safe generic ALTER INDEX for ${qualifiedName}.\n-- Inspect the definition and use the engine-specific operation.`; } if (objectType === 'trigger') { if (engineId === 'postgresql' && tableQualifiedName) { - return `ALTER TRIGGER ${identifierName} ON ${tableQualifiedName}\n -- RENAME TO nuevo_nombre;`; + return `ALTER TRIGGER ${identifierName} ON ${tableQualifiedName}\n -- RENAME TO new_name;`; } if (engineId === 'oracle') { return `ALTER TRIGGER ${qualifiedName} COMPILE;\n-- ALTER TRIGGER ${qualifiedName} ENABLE;`; } if (engineId === 'sqlserver' && tableQualifiedName) { - return `ALTER TRIGGER ${qualifiedName}\nON ${tableQualifiedName}\nAFTER INSERT\nAS\nBEGIN\n SET NOCOUNT ON;\n -- implementación\nEND;`; + return `ALTER TRIGGER ${qualifiedName}\nON ${tableQualifiedName}\nAFTER INSERT\nAS\nBEGIN\n SET NOCOUNT ON;\n -- implementation\nEND;`; } if (['mysql', 'sqlite'].includes(engineId)) { - return `-- ${engineId === 'mysql' ? 'MySQL' : 'SQLite'} no permite alterar el cuerpo de un trigger directamente.\n-- Consulta su definición y utiliza DROP TRIGGER + CREATE TRIGGER para ${qualifiedName}.`; + return `-- ${engineId === 'mysql' ? 'MySQL' : 'SQLite'} does not allow a trigger body to be altered directly.\n-- Inspect its definition and use DROP TRIGGER + CREATE TRIGGER for ${qualifiedName}.`; } - return `-- Edita la definición del trigger ${qualifiedName} con la sintaxis específica del motor.`; + return `-- Edit the ${qualifiedName} trigger definition using engine-specific syntax.`; } if (objectType === 'synonym') { - return `-- Los sinónimos se sustituyen normalmente con DROP + CREATE.\n-- Objeto: ${qualifiedName}`; + return `-- Synonyms are normally replaced with DROP + CREATE.\n-- Object: ${qualifiedName}`; } return `-- ALTER ${OBJECT_LABELS[objectType] || objectType}: ${qualifiedName}`; } @@ -281,7 +281,7 @@ function dropTemplate(engineId, objectType, qualifiedName, options = {}) { event: 'EVENT', }; const keyword = keywords[objectType]; - if (!keyword) throw new Error(`Tipo de objeto no válido: ${objectType}`); + if (!keyword) throw new Error(`Invalid object type: ${objectType}`); if (objectType === 'index' && options.tableQualifiedName) { if (engineId === 'mysql' && String(options.objectName || '').toUpperCase() === 'PRIMARY') { diff --git a/src/sql/transactionControl.js b/src/sql/transactionControl.js index e6a250c..01ee433 100644 --- a/src/sql/transactionControl.js +++ b/src/sql/transactionControl.js @@ -14,15 +14,15 @@ function normalizeControlSql(sql, engineId) { } function detectTransactionControl(sql, engineId) { - // El worker SQLite sincroniza también BEGIN IMMEDIATE/EXCLUSIVE y SAVEPOINT. + // The SQLite worker also synchronizes BEGIN IMMEDIATE/EXCLUSIVE and SAVEPOINT. if (engineId === 'sqlite') return null; const code = normalizeControlSql(sql, engineId); if (engineId !== 'oracle') { const beginPatterns = { - // PostgreSQL permite modos como ISOLATION LEVEL / READ ONLY y MySQL - // añade READ ONLY/WRITE o WITH CONSISTENT SNAPSHOT. Se reserva primero - // la conexión física y el adaptador ejecuta la sentencia original. + // PostgreSQL supports modes such as ISOLATION LEVEL / READ ONLY, while + // MySQL adds READ ONLY/WRITE or WITH CONSISTENT SNAPSHOT. Reserve the + // physical connection first, then let the adapter execute the original SQL. postgresql: /^(?:BEGIN(?:\s+(?:WORK|TRANSACTION))?(?:\s+.+)?|START\s+TRANSACTION(?:\s+.+)?)$/i, mysql: /^(?:BEGIN(?:\s+WORK)?|START\s+TRANSACTION(?:\s+.+)?)$/i, sqlserver: /^BEGIN\s+TRAN(?:SACTION)?(?:\s+[A-Za-z_][A-Za-z0-9_@$#]*)?$/i, diff --git a/src/storage/connectionStore.js b/src/storage/connectionStore.js index 84132a7..bb74d1f 100644 --- a/src/storage/connectionStore.js +++ b/src/storage/connectionStore.js @@ -103,13 +103,13 @@ class ConnectionStore { }); if (!profile.name) { - throw new Error('El nombre de la conexión es obligatorio.'); + throw new Error('Connection name is required.'); } if (!isDatabaseEngineId(profile.engine)) { - throw new Error(`Motor de base de datos no válido: ${profile.engine}`); + throw new Error(`Invalid database engine: ${profile.engine}`); } if (profile.engine === 'sqlite' && !profile.filePath) { - throw new Error('La ruta del archivo SQLite es obligatoria.'); + throw new Error('SQLite file path is required.'); } const profiles = this.list(); @@ -121,7 +121,7 @@ class ConnectionStore { }) === 0, ); if (duplicate) { - throw new Error(`Ya existe una conexión llamada "${profile.name}".`); + throw new Error(`A connection named "${profile.name}" already exists.`); } const index = profiles.findIndex((candidate) => candidate.id === id); diff --git a/src/storage/historyStore.js b/src/storage/historyStore.js index 1539170..bf23988 100644 --- a/src/storage/historyStore.js +++ b/src/storage/historyStore.js @@ -53,7 +53,7 @@ class HistoryStore { schema: entry.schema || '', sql: sql.length > MAX_SQL_CHARACTERS - ? `${sql.slice(0, MAX_SQL_CHARACTERS)}\n-- [Historial truncado]` + ? `${sql.slice(0, MAX_SQL_CHARACTERS)}\n-- [History truncated]` : sql, durationMs: Number(entry.durationMs || 0), rows: Number(entry.rows || 0), diff --git a/src/storage/resultStore.js b/src/storage/resultStore.js index 76f514e..af21444 100644 --- a/src/storage/resultStore.js +++ b/src/storage/resultStore.js @@ -71,7 +71,7 @@ class ResultStore { async startSet(executionId, setIndex, metadata = {}) { const execution = this.executions.get(executionId); if (!execution) { - throw new Error(`Resultado desconocido: ${executionId}`); + throw new Error(`Unknown result: ${executionId}`); } const columns = (metadata.columns || []).map((column, index) => ({ @@ -127,7 +127,7 @@ class ResultStore { } const state = this.setStates.get(this._setKey(executionId, setIndex)); if (!state) { - throw new Error(`Conjunto de resultados desconocido: ${setIndex}`); + throw new Error(`Unknown result set: ${setIndex}`); } const normalized = normalizeRows( diff --git a/src/test/adapterFactory.test.js b/src/test/adapterFactory.test.js index 6037c32..964849a 100644 --- a/src/test/adapterFactory.test.js +++ b/src/test/adapterFactory.test.js @@ -11,7 +11,7 @@ const profiles = [ ]; describe('adapter factory contract', () => { - it.each(profiles)('carga el adaptador $engine y cumple el contrato común', (profile) => { + it.each(profiles)('loads the $engine adapter and satisfies the common contract', (profile) => { const adapter = createAdapter(profile, 'password'); for (const method of [ 'connect', @@ -35,7 +35,7 @@ describe('adapter factory contract', () => { } }); - it('rechaza motores desconocidos', () => { - expect(() => createAdapter({ engine: 'unknown' }, '')).toThrow(/no soportado/i); + it('rejects unknown engines', () => { + expect(() => createAdapter({ engine: 'unknown' }, '')).toThrow(/unsupported database engine/i); }); }); diff --git a/src/test/connectionStore.test.js b/src/test/connectionStore.test.js index daa2f3d..32b8cdc 100644 --- a/src/test/connectionStore.test.js +++ b/src/test/connectionStore.test.js @@ -22,13 +22,13 @@ function memorySecrets() { } describe('ConnectionStore', () => { - it('guarda el perfil separado de la contraseña', async () => { + it('stores the profile separately from the password', async () => { const state = memoryMemento(); const secrets = memorySecrets(); const store = new ConnectionStore(state, secrets); const profile = await store.save( { - name: 'Producción PG', + name: 'Production PG', engine: 'postgresql', host: 'db.internal', port: 5432, @@ -44,7 +44,7 @@ describe('ConnectionStore', () => { expect(store.get(profile.id)).not.toHaveProperty('password'); }); - it('conserva un secreto existente al editar si no se proporciona contraseña', async () => { + it('keeps an existing secret when editing without a new password', async () => { const store = new ConnectionStore(memoryMemento(), memorySecrets()); const profile = await store.save( { name: 'MySQL', engine: 'mysql', host: 'localhost', port: 3306, user: 'u' }, @@ -56,7 +56,7 @@ describe('ConnectionStore', () => { await expect(store.getPassword(profile.id)).resolves.toBe('old-password'); }); - it('elimina perfil y secreto y evita nombres duplicados', async () => { + it('deletes profile and secret and prevents duplicate names', async () => { const state = memoryMemento(); const secrets = memorySecrets(); const store = new ConnectionStore(state, secrets); @@ -69,7 +69,7 @@ describe('ConnectionStore', () => { { name: 'Oracle', engine: 'sqlite', filePath: '/tmp/other.db' }, '', ), - ).rejects.toThrow(/Ya existe/); + ).rejects.toThrow(/already exists/i); await store.delete(one.id); expect(store.get(one.id)).toBeUndefined(); await expect(store.getPassword(one.id)).resolves.toBe(''); diff --git a/src/test/databaseEngines.test.js b/src/test/databaseEngines.test.js index 4a1f6a3..896bdd0 100644 --- a/src/test/databaseEngines.test.js +++ b/src/test/databaseEngines.test.js @@ -8,7 +8,7 @@ const { } = require('../databaseEngines'); describe('databaseEngines', () => { - it('expone exactamente los cinco motores soportados', () => { + it('exposes exactly the five supported database engines', () => { expect(DATABASE_ENGINE_IDS).toEqual([ 'sqlite', 'postgresql', @@ -19,7 +19,7 @@ describe('databaseEngines', () => { expect(DATABASE_ENGINES).toHaveLength(5); }); - it('define puertos y grupos de objetos propios de cada motor', () => { + it('defines engine-specific ports and object groups', () => { expect(getDatabaseEngine('sqlite').defaultPort).toBeNull(); expect(getDatabaseEngine('postgresql').defaultPort).toBe(5432); expect(getDatabaseEngine('mysql').defaultPort).toBe(3306); @@ -30,7 +30,7 @@ describe('databaseEngines', () => { expect(getDatabaseEngine('mysql').objectGroups).toContain('events'); }); - it('valida identificadores de motor sin aceptar valores desconocidos', () => { + it('validates engine identifiers without accepting unknown values', () => { expect(isDatabaseEngineId('oracle')).toBe(true); expect(isDatabaseEngineId('mongo')).toBe(false); expect(getDatabaseEngine('mongo')).toBeUndefined(); diff --git a/src/test/ddlTemplates.test.js b/src/test/ddlTemplates.test.js index aa1f803..b5065df 100644 --- a/src/test/ddlTemplates.test.js +++ b/src/test/ddlTemplates.test.js @@ -11,7 +11,7 @@ const { const quote = (value) => `"${value}"`; describe('DDL templates', () => { - it('ofrece CREATE/ALTER/DROP para todos los tipos anunciados', () => { + it('provides CREATE/ALTER/DROP for every advertised object type', () => { for (const [engine, types] of Object.entries(OBJECT_TYPES_BY_ENGINE)) { expect(objectTypesForEngine(engine)).toEqual(types); for (const type of types) { @@ -27,7 +27,7 @@ describe('DDL templates', () => { } }); - it('crea un DROP INDEX correcto para MySQL y SQL Server cuando conoce la tabla', () => { + it('creates the correct DROP INDEX for MySQL and SQL Server when the table is known', () => { expect( dropTemplate('mysql', 'index', '`db`.`idx`', { identifierName: '`idx`', @@ -42,7 +42,7 @@ describe('DDL templates', () => { ).toBe('DROP INDEX [idx] ON [dbo].[items];'); }); - it('respeta las reglas DDL especiales de índices, triggers y constraints', () => { + it('respects special DDL rules for indexes, triggers, and constraints', () => { expect( createTemplate('postgresql', 'index', '"public"."idx_items"', quote, { identifierName: '"idx_items"', @@ -81,7 +81,7 @@ describe('DDL templates', () => { ).toBe('ALTER TABLE "public"."items" DROP CONSTRAINT IF EXISTS "items_pkey";'); }); - it('genera ALTER específico cuando el motor necesita el nombre de tabla', () => { + it('generates engine-specific ALTER when a table name is required', () => { expect( alterTemplate('sqlserver', 'index', '[dbo].[idx]', { identifierName: '[idx]', @@ -96,7 +96,7 @@ describe('DDL templates', () => { ).toContain('ALTER TRIGGER "trg" ON "public"."items"'); }); - it('no introduce TOP/LIMIT/FETCH en las plantillas SELECT', () => { + it('does not introduce TOP/LIMIT/FETCH in SELECT templates', () => { for (const engine of Object.keys(OBJECT_TYPES_BY_ENGINE)) { if (!objectTypesForEngine(engine).includes('view')) continue; const sql = createTemplate(engine, 'view', 'demo', quote); diff --git a/src/test/resultStore.test.js b/src/test/resultStore.test.js index 1ff2329..807c32f 100644 --- a/src/test/resultStore.test.js +++ b/src/test/resultStore.test.js @@ -23,7 +23,7 @@ describe('ResultStore', () => { await fs.rm(temporary, { recursive: true, force: true }); }); - it('pagina en disco sin truncar el número de filas', async () => { + it('pages results on disk without truncating the row count', async () => { await store.createExecution('exec-1', { connectionName: 'test' }); await store.startSet('exec-1', 0, { columns: [{ name: 'id', type: 'INTEGER' }], @@ -42,7 +42,7 @@ describe('ResultStore', () => { expect(allRows).toEqual([[1], [2], [3], [4], [5]]); }); - it('permite reconfigurar el tamaño de página para ejecuciones nuevas', async () => { + it('allows page size to be reconfigured for new executions', async () => { store.configure({ pageSize: 3, maxCellCharacters: 5 }); await store.createExecution('exec-2', {}); await store.startSet('exec-2', 0, { columns: [{ name: 'text' }] }); diff --git a/src/test/safety.test.js b/src/test/safety.test.js index 0616d4a..23201e6 100644 --- a/src/test/safety.test.js +++ b/src/test/safety.test.js @@ -3,7 +3,7 @@ const { classifySqlSafety, looksLikeRowQuery } = require('../sql/safety'); describe('SQL safety', () => { - it('detecta DROP/TRUNCATE reales pero no texto dentro de strings/comentarios', () => { + it('detects real DROP/TRUNCATE statements but ignores text in strings/comments', () => { expect(classifySqlSafety('DROP TABLE demo;', 'postgresql').destructive).toBe(true); expect(classifySqlSafety('TRUNCATE TABLE demo;', 'mysql').destructive).toBe(true); expect(classifySqlSafety("SELECT 'DROP TABLE demo'", 'postgresql').destructive).toBe(false); @@ -12,13 +12,13 @@ describe('SQL safety', () => { expect(classifySqlSafety("SELECT q'[DROP TABLE demo]' FROM dual", 'oracle').destructive).toBe(false); }); - it('avisa de UPDATE/DELETE sin WHERE', () => { + it('flags UPDATE/DELETE without WHERE', () => { expect(classifySqlSafety('UPDATE demo SET active = 0;', 'sqlite').unsafeDml).toBe(true); expect(classifySqlSafety('DELETE FROM demo;', 'oracle').unsafeDml).toBe(true); expect(classifySqlSafety('UPDATE demo SET active = 0 WHERE id = 1;', 'sqlite').unsafeDml).toBe(false); }); - it('reconoce consultas que producen filas', () => { + it('recognizes queries that return rows', () => { for (const sql of ['SELECT 1', 'WITH x AS (SELECT 1) SELECT * FROM x', 'SHOW TABLES', 'PRAGMA table_info(x)']) { expect(looksLikeRowQuery(sql, 'postgresql')).toBe(true); } diff --git a/src/test/sqlSplitter.test.js b/src/test/sqlSplitter.test.js index ccaa24c..a75ccc4 100644 --- a/src/test/sqlSplitter.test.js +++ b/src/test/sqlSplitter.test.js @@ -8,20 +8,20 @@ const { const { wrapMysqlDefinition } = require('../adapters/mysqlAdapter'); describe('sqlSplitter', () => { - it('ignora delimitadores dentro de strings y comentarios', () => { - const sql = "SELECT ';' AS value; -- ; oculto\nSELECT 2 /* ; */;"; + it('ignores delimiters inside strings and comments', () => { + const sql = "SELECT ';' AS value; -- ; hidden\nSELECT 2 /* ; */;"; const statements = splitSqlDocument(sql, 'sqlite'); expect(statements).toHaveLength(2); expect(statements[0].sql).toContain("';'"); expect(statements[1].sql).toContain('SELECT 2'); }); - it('mantiene cuerpos dollar-quoted de PostgreSQL como una sentencia', () => { + it('keeps PostgreSQL dollar-quoted bodies as one statement', () => { const sql = `CREATE OR REPLACE FUNCTION public.demo() RETURNS void LANGUAGE plpgsql AS $$ BEGIN PERFORM 1; - RAISE NOTICE 'hola;'; + RAISE NOTICE 'hello;'; END; $$; SELECT 42;`; @@ -31,7 +31,7 @@ SELECT 42;`; expect(statements[1].sql).toBe('SELECT 42;'); }); - it('interpreta GO y GO n en SQL Server sin enviarlos al servidor', () => { + it('handles SQL Server GO and GO n without sending them to the server', () => { const sql = 'SELECT 1;\nGO 2\nSELECT 2;\nGO\n'; const statements = splitSqlDocument(sql, 'sqlserver'); expect(statements.map((entry) => entry.sql)).toEqual([ @@ -41,7 +41,7 @@ SELECT 42;`; ]); }); - it('interpreta DELIMITER de MySQL y conserva el cuerpo de la rutina', () => { + it('handles MySQL DELIMITER and preserves the routine body', () => { const sql = `DELIMITER $$ CREATE PROCEDURE demo() BEGIN @@ -57,7 +57,7 @@ SELECT 3;`; expect(statements[1].sql).toBe('SELECT 3;'); }); - it('reabre SHOW CREATE de rutinas MySQL como un bloque ejecutable único', () => { + it('reopens MySQL SHOW CREATE routines as a single executable block', () => { const definition = wrapMysqlDefinition( 'CREATE PROCEDURE `demo`() BEGIN SELECT 1; SELECT 2; END', ); @@ -67,13 +67,13 @@ SELECT 3;`; expect(statements[0].sql).not.toContain('DELIMITER'); }); - it('ignora delimitadores dentro de comentarios # de MySQL', () => { - const statements = splitSqlDocument('# comentario; oculto\nSELECT 1;', 'mysql'); + it('ignores delimiters inside MySQL # comments', () => { + const statements = splitSqlDocument('# comment; hidden\nSELECT 1;', 'mysql'); expect(statements).toHaveLength(1); expect(statements[0].sql).toContain('SELECT 1;'); }); - it('mantiene bloques PL/SQL Oracle hasta la barra de terminación', () => { + it('keeps Oracle PL/SQL blocks through the terminating slash', () => { const sql = `CREATE OR REPLACE PROCEDURE demo AS BEGIN NULL; @@ -87,17 +87,17 @@ SELECT 1 FROM dual;`; expect(statements[1].sql).toBe('SELECT 1 FROM dual;'); }); - it('respeta literales q-quoted de Oracle al dividir SQL normal', () => { - const sql = "INSERT INTO demo(value) VALUES (q'[uno;dos]'); SELECT 2 FROM dual;"; + it('respects Oracle q-quoted literals when splitting regular SQL', () => { + const sql = "INSERT INTO demo(value) VALUES (q'[one;two]'); SELECT 2 FROM dual;"; const statements = splitSqlDocument(sql, 'oracle'); expect(statements).toHaveLength(2); - expect(statements[0].sql).toContain("q'[uno;dos]'"); + expect(statements[0].sql).toContain("q'[one;two]'"); }); - it('mantiene un CREATE TRIGGER SQLite con sentencias internas', () => { + it('keeps a SQLite CREATE TRIGGER with internal statements intact', () => { const sql = `CREATE TRIGGER audit_insert AFTER INSERT ON items BEGIN - INSERT INTO audit(message) VALUES ('uno;dos'); + INSERT INTO audit(message) VALUES ('one;two'); UPDATE counters SET value = value + 1; END; SELECT * FROM items;`; @@ -107,18 +107,18 @@ SELECT * FROM items;`; expect(statements[1].sql).toBe('SELECT * FROM items;'); }); - it('localiza la sentencia bajo el cursor', () => { + it('finds the statement under the cursor', () => { const sql = 'SELECT 1;\n\nSELECT 22;\nSELECT 333;'; const offset = sql.indexOf('22') + 1; expect(findStatementAtOffset(sql, 'postgresql', offset).sql).toBe('SELECT 22;'); }); - it('enmascara literales y comentarios sin cambiar offsets ni saltos de línea', () => { - const sql = "SELECT 'secreto;'; -- comentario\nSELECT 1;"; + it('masks literals and comments without changing offsets or line breaks', () => { + const sql = "SELECT 'secret;'; -- comment\nSELECT 1;"; const mask = createCodeMask(sql); expect(mask).toHaveLength(sql.length); expect(mask.split('\n')).toHaveLength(sql.split('\n').length); - expect(mask).not.toContain('secreto'); - expect(mask).not.toContain('comentario'); + expect(mask).not.toContain('secret'); + expect(mask).not.toContain('comment'); }); }); diff --git a/src/test/sqliteAdapter.test.js b/src/test/sqliteAdapter.test.js index e3a60bc..65de806 100644 --- a/src/test/sqliteAdapter.test.js +++ b/src/test/sqliteAdapter.test.js @@ -54,7 +54,7 @@ describe('SqliteAdapter integration', () => { return { result, sink }; } - it('crea, modifica y consulta datos sin límite de filas por defecto', async () => { + it('creates, modifies, and queries data with no default row limit', async () => { await execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL);'); await execute("INSERT INTO items(name) VALUES ('a'), ('b'), ('c'), ('d'), ('e');"); const { result, sink } = await execute('SELECT id, name FROM items ORDER BY id;'); @@ -65,7 +65,7 @@ describe('SqliteAdapter integration', () => { expect(sink.sets[0].rows[4][1]).toBe('e'); }); - it('sólo limita filas cuando maxRows es mayor que cero', async () => { + it('limits rows only when maxRows is greater than zero', async () => { await execute('CREATE TABLE numbers (value INTEGER);'); await execute('INSERT INTO numbers VALUES (1),(2),(3),(4);'); const { result, sink } = await execute('SELECT value FROM numbers ORDER BY value;', 2); @@ -74,14 +74,14 @@ describe('SqliteAdapter integration', () => { expect(sink.sets[0].rows).toEqual([[1], [2]]); }); - it('conserva enteros SQLite de 64 bits como BigInt hasta la normalización', async () => { + it('keeps 64-bit SQLite integers as BigInt until normalization', async () => { await execute('CREATE TABLE big_numbers (value INTEGER);'); await execute('INSERT INTO big_numbers VALUES (9223372036854775807);'); const { sink } = await execute('SELECT value FROM big_numbers;'); expect(sink.sets[0].rows[0][0]).toBe(9223372036854775807n); }); - it('hace ROLLBACK y COMMIT de transacciones explícitas', async () => { + it('ROLLBACKs and COMMITs explicit transactions', async () => { await execute('CREATE TABLE tx (value TEXT);'); await adapter.begin('session'); await execute("INSERT INTO tx VALUES ('rollback');"); @@ -94,7 +94,7 @@ describe('SqliteAdapter integration', () => { expect((await execute('SELECT COUNT(*) AS total FROM tx;')).sink.sets[0].rows[0][0]).toBe(1); }); - it('sincroniza BEGIN/ROLLBACK escritos como SQL y aísla otros editores', async () => { + it('synchronizes SQL BEGIN/ROLLBACK and isolates other editors', async () => { await execute('CREATE TABLE raw_tx (value TEXT);'); await execute('BEGIN;'); expect(adapter.hasTransaction('session')).toBe(true); @@ -106,14 +106,14 @@ describe('SqliteAdapter integration', () => { pageSize: 2, sink: otherSink, }), - ).rejects.toThrow(/otro editor/i); - await execute("INSERT INTO raw_tx VALUES ('no guardar');"); + ).rejects.toThrow(/another editor/i); + await execute("INSERT INTO raw_tx VALUES ('do not save');"); await execute('ROLLBACK;'); expect(adapter.hasTransaction('session')).toBe(false); expect((await execute('SELECT COUNT(*) FROM raw_tx;')).sink.sets[0].rows[0][0]).toBe(0); }); - it('mantiene foreign_keys activo después de persistir el archivo', async () => { + it('keeps foreign_keys enabled after persisting the file', async () => { await execute('CREATE TABLE parent (id INTEGER PRIMARY KEY);'); await execute('CREATE TABLE child (parent_id INTEGER REFERENCES parent(id));'); await expect(execute('INSERT INTO child(parent_id) VALUES (999);')).rejects.toThrow( @@ -121,29 +121,29 @@ describe('SqliteAdapter integration', () => { ); }); - it('se niega a sobrescribir un archivo modificado externamente', async () => { + it('refuses to overwrite a file modified externally', async () => { await execute('CREATE TABLE conflict (id INTEGER);'); const filename = path.join(temporary, 'database.sqlite'); const stat = await fs.stat(filename); const changed = new Date(stat.mtimeMs + 5000); await fs.utimes(filename, changed, changed); await expect(execute('INSERT INTO conflict VALUES (1);')).rejects.toThrow( - /modificado por otra aplicación/i, + /modified by another application/i, ); }); - it('obliga a reconectar también antes de leer si el archivo cambió externamente', async () => { + it('requires reconnecting before reads when the file changed externally', async () => { await execute('CREATE TABLE stale_guard (id INTEGER);'); const filename = path.join(temporary, 'database.sqlite'); const stat = await fs.stat(filename); const changed = new Date(stat.mtimeMs + 5000); await fs.utimes(filename, changed, changed); await expect(execute('SELECT * FROM stale_guard;')).rejects.toThrow( - /modificado por otra aplicación/i, + /modified by another application/i, ); }); - it('rechaza un WAL activo para no cargar una instantánea incompleta', async () => { + it('rejects an active WAL to avoid loading an incomplete snapshot', async () => { await execute('CREATE TABLE wal_guard (id INTEGER);'); const filename = path.join(temporary, 'database.sqlite'); await adapter.disconnect(); @@ -159,10 +159,10 @@ describe('SqliteAdapter integration', () => { }, '', ); - await expect(adapter.connect()).rejects.toThrow(/WAL activo/i); + await expect(adapter.connect()).rejects.toThrow(/active WAL/i); }); - it('explora tablas, columnas, índices, triggers y sus definiciones', async () => { + it('explores tables, columns, indexes, triggers, and their definitions', async () => { await execute('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL);'); await execute('CREATE INDEX idx_items_name ON items(name);'); await execute(`CREATE TRIGGER trg_items AFTER INSERT ON items diff --git a/src/test/transactionControl.test.js b/src/test/transactionControl.test.js index 3bb6324..24aa126 100644 --- a/src/test/transactionControl.test.js +++ b/src/test/transactionControl.test.js @@ -3,7 +3,7 @@ const { detectTransactionControl } = require('../sql/transactionControl'); describe('transactionControl', () => { - it('detecta controles explícitos habituales de PostgreSQL y MySQL', () => { + it('detects common explicit transaction controls in PostgreSQL and MySQL', () => { expect(detectTransactionControl('BEGIN;', 'postgresql')).toBe('begin'); expect(detectTransactionControl('START TRANSACTION;', 'mysql')).toBe('begin'); expect( @@ -16,13 +16,13 @@ describe('transactionControl', () => { expect(detectTransactionControl('ROLLBACK;', 'mysql')).toBe('rollback'); }); - it('detecta BEGIN TRANSACTION de SQL Server pero no BEGIN TRY', () => { + it('detects SQL Server BEGIN TRANSACTION but not BEGIN TRY', () => { expect(detectTransactionControl('BEGIN TRANSACTION;', 'sqlserver')).toBe('begin'); expect(detectTransactionControl('BEGIN TRAN tx_name;', 'sqlserver')).toBe('begin'); expect(detectTransactionControl('BEGIN TRY', 'sqlserver')).toBeNull(); }); - it('no confunde bloques Oracle ni transacciones SQLite gestionadas por su worker', () => { + it('does not confuse Oracle blocks or SQLite transactions managed by its worker', () => { expect(detectTransactionControl('BEGIN NULL; END;', 'oracle')).toBeNull(); expect(detectTransactionControl('BEGIN IMMEDIATE;', 'sqlite')).toBeNull(); expect(detectTransactionControl('ROLLBACK TO SAVEPOINT x;', 'postgresql')).toBeNull(); diff --git a/src/test/valueNormalizer.test.js b/src/test/valueNormalizer.test.js index e5ad267..9705e9b 100644 --- a/src/test/valueNormalizer.test.js +++ b/src/test/valueNormalizer.test.js @@ -3,7 +3,7 @@ const { normalizeRows, normalizeValue, truncateText } = require('../core/valueNormalizer'); describe('valueNormalizer', () => { - it('conserva tipos escalares y normaliza valores especiales', () => { + it('preserves scalar types and normalizes special values', () => { expect(normalizeValue(null)).toBeNull(); expect(normalizeValue(42)).toBe(42); expect(normalizeValue(true)).toBe(true); @@ -13,7 +13,7 @@ describe('valueNormalizer', () => { ); }); - it('limita celdas grandes de forma explícita y no filas', () => { + it('explicitly limits large cells, not rows', () => { expect(truncateText('abcdefghij', 5)).toContain('abcde…'); const rows = normalizeRows( [{ value: 'abcdefghij' }, { value: 'klmnopqrst' }], @@ -24,7 +24,7 @@ describe('valueNormalizer', () => { expect(rows[0][0]).toContain('omitidos'); }); - it('genera una vista segura para binarios', () => { + it('generates a safe display value for binary data', () => { expect(normalizeValue(Buffer.from([0, 1, 2]))).toContain(''); }); }); diff --git a/src/test/xlsx.test.js b/src/test/xlsx.test.js index 3ca4d8d..a69d7e5 100644 --- a/src/test/xlsx.test.js +++ b/src/test/xlsx.test.js @@ -6,12 +6,12 @@ const path = require('node:path'); const ExcelJS = require('exceljs'); describe('XLSX export dependency', () => { - it('escribe y vuelve a leer un XLSX con el writer de streaming', async () => { + it('writes and reads an XLSX with the streaming writer', async () => { const temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'simple-db-xlsx-')); const filename = path.join(temporary, 'result.xlsx'); try { const output = new ExcelJS.stream.xlsx.WorkbookWriter({ filename }); - const sheet = output.addWorksheet('Resultado'); + const sheet = output.addWorksheet('Result'); sheet.addRow(['id', 'name']).commit(); sheet.addRow([1, 'demo']).commit(); sheet.commit(); @@ -19,7 +19,7 @@ describe('XLSX export dependency', () => { const input = new ExcelJS.Workbook(); await input.xlsx.readFile(filename); - expect(input.getWorksheet('Resultado').getRow(2).values.slice(1)).toEqual([ + expect(input.getWorksheet('Result').getRow(2).values.slice(1)).toEqual([ 1, 'demo', ]); diff --git a/src/ui/connectionForm.js b/src/ui/connectionForm.js index 47e60ab..20194e8 100644 --- a/src/ui/connectionForm.js +++ b/src/ui/connectionForm.js @@ -11,7 +11,7 @@ async function inputText(options) { password: options.password === true, ignoreFocusOut: true, validateInput: options.required - ? (value) => (value.trim() ? undefined : 'Este campo es obligatorio.') + ? (value) => (value.trim() ? undefined : 'This field is required.') : options.validateInput, }); } @@ -29,7 +29,7 @@ async function inputNumber(options) { const number = Number(result); if (!Number.isInteger(number) || number < (options.minimum ?? 0)) { await vscode.window.showErrorMessage( - `${options.prompt}: introduce un número entero válido.`, + `${options.prompt}: enter a valid integer.`, ); return inputNumber(options); } @@ -39,8 +39,8 @@ async function inputNumber(options) { async function inputBoolean(title, label, value) { const picked = await vscode.window.showQuickPick( [ - { label: value ? 'Sí' : 'No', value }, - { label: value ? 'No' : 'Sí', value: !value }, + { label: value ? 'Yes' : 'No', value }, + { label: value ? 'No' : 'Yes', value: !value }, ], { title, @@ -54,8 +54,8 @@ async function inputBoolean(title, label, value) { async function chooseSqlitePath(existingProfile) { if (existingProfile) { return inputText({ - title: 'Simple DB — Editar SQLite', - prompt: 'Ruta completa del archivo SQLite', + title: 'Simple DB — Edit SQLite', + prompt: 'Full SQLite file path', value: existingProfile.filePath, required: true, }); @@ -63,13 +63,13 @@ async function chooseSqlitePath(existingProfile) { const mode = await vscode.window.showQuickPick( [ - { label: '$(folder-opened) Abrir archivo existente', value: 'open' }, - { label: '$(new-file) Crear archivo nuevo', value: 'create' }, - { label: '$(edit) Escribir la ruta manualmente', value: 'manual' }, + { label: '$(folder-opened) Open Existing File', value: 'open' }, + { label: '$(new-file) Create New File', value: 'create' }, + { label: '$(edit) Enter Path Manually', value: 'manual' }, ], { - title: 'Simple DB — Archivo SQLite', - placeHolder: 'Selecciona cómo indicar el archivo SQLite', + title: 'Simple DB — SQLite File', + placeHolder: 'Choose how to specify the SQLite file', ignoreFocusOut: true, }, ); @@ -82,10 +82,10 @@ async function chooseSqlitePath(existingProfile) { canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - title: 'Seleccionar base de datos SQLite', + title: 'Select SQLite Database', filters: { 'SQLite database': ['db', 'sqlite', 'sqlite3'], - 'Todos los archivos': ['*'], + 'All files': ['*'], }, }); return selected?.[0]?.fsPath; @@ -93,15 +93,15 @@ async function chooseSqlitePath(existingProfile) { if (mode.value === 'create') { const selected = await vscode.window.showSaveDialog({ - title: 'Crear base de datos SQLite', + title: 'Create SQLite Database', filters: { 'SQLite database': ['db', 'sqlite', 'sqlite3'] }, }); return selected?.fsPath; } return inputText({ - title: 'Simple DB — Archivo SQLite', - prompt: 'Ruta completa del archivo SQLite', + title: 'Simple DB — SQLite File', + prompt: 'Full SQLite file path', required: true, }); } @@ -115,12 +115,12 @@ async function promptConnection(options = {}) { DATABASE_ENGINES.map((engine) => ({ label: `$(database) ${engine.displayName}`, description: - engine.defaultPort === null ? 'Archivo local' : `Puerto ${engine.defaultPort}`, + engine.defaultPort === null ? 'Local file' : `Port ${engine.defaultPort}`, value: engine.id, })), { - title: 'Simple DB — Nueva conexión', - placeHolder: 'Selecciona el motor de base de datos', + title: 'Simple DB — New Connection', + placeHolder: 'Select a database engine', ignoreFocusOut: true, }, ); @@ -132,12 +132,12 @@ async function promptConnection(options = {}) { const engine = getDatabaseEngine(engineId); if (!engine) { - throw new Error(`Motor no válido: ${engineId}`); + throw new Error(`Invalid database engine: ${engineId}`); } const name = await inputText({ - title: `Simple DB — ${existing ? 'Editar' : 'Nueva'} conexión ${engine.displayName}`, - prompt: 'Nombre visible de la conexión', + title: `Simple DB — ${existing ? 'Edit' : 'New'} ${engine.displayName} Connection`, + prompt: 'Connection display name', value: existing?.name || '', required: true, }); @@ -159,7 +159,7 @@ async function promptConnection(options = {}) { } const readOnly = await inputBoolean( 'Simple DB — SQLite', - '¿Abrir en modo solo lectura?', + 'Open in read-only mode?', existing?.readOnly || false, ); if (readOnly === undefined) { @@ -171,19 +171,19 @@ async function promptConnection(options = {}) { const mode = await vscode.window.showQuickPick( [ { - label: 'Host + puerto + servicio/PDB', + label: 'Host + port + service/PDB', value: 'service', - description: 'Formato habitual de Oracle Easy Connect', + description: 'Standard Oracle Easy Connect format', }, { label: 'Connect string / alias TNS', value: 'connectString', - description: 'Usar una cadena de conexión directamente', + description: 'Use a connection string directly', }, ], { title: 'Simple DB — Oracle', - placeHolder: 'Modo de conexión Oracle', + placeHolder: 'Oracle connection mode', ignoreFocusOut: true, }, ); @@ -194,21 +194,21 @@ async function promptConnection(options = {}) { if (mode.value === 'service') { profile.host = await inputText({ title: 'Simple DB — Oracle', - prompt: 'Servidor / host', + prompt: 'Server / host', value: existing?.host || 'localhost', required: true, }); if (profile.host === undefined) return null; profile.port = await inputNumber({ title: 'Simple DB — Oracle', - prompt: 'Puerto', + prompt: 'Port', value: existing?.port || 1521, minimum: 1, }); if (profile.port === undefined) return null; profile.serviceName = await inputText({ title: 'Simple DB — Oracle', - prompt: 'Servicio / PDB', + prompt: 'Service / PDB', value: existing?.serviceName || existing?.database || '', required: true, }); @@ -218,7 +218,7 @@ async function promptConnection(options = {}) { } else { profile.connectString = await inputText({ title: 'Simple DB — Oracle', - prompt: 'Connect string o alias TNS', + prompt: 'Connect string or TNS alias', value: existing?.connectString || '', required: true, }); @@ -231,14 +231,14 @@ async function promptConnection(options = {}) { profile.user = await inputText({ title: 'Simple DB — Oracle', - prompt: 'Usuario', + prompt: 'Username', value: existing?.user || '', required: true, }); if (profile.user === undefined) return null; password = await inputText({ title: 'Simple DB — Oracle', - prompt: existing ? 'Contraseña (vacío = conservar la actual)' : 'Contraseña', + prompt: existing ? 'Password (leave empty to keep the current one)' : 'Password', password: true, required: false, }); @@ -246,7 +246,7 @@ async function promptConnection(options = {}) { } else { profile.host = await inputText({ title: `Simple DB — ${engine.displayName}`, - prompt: 'Servidor / host', + prompt: 'Server / host', value: existing?.host || 'localhost', required: true, }); @@ -255,7 +255,7 @@ async function promptConnection(options = {}) { if (engineId === 'sqlserver') { profile.instanceName = await inputText({ title: 'Simple DB — SQL Server', - prompt: 'Instancia (opcional; vacío = usar puerto TCP)', + prompt: 'Instance (optional; leave empty to use the TCP port)', value: existing?.instanceName || '', }); if (profile.instanceName === undefined) return null; @@ -263,7 +263,7 @@ async function promptConnection(options = {}) { profile.port = await inputNumber({ title: `Simple DB — ${engine.displayName}`, - prompt: 'Puerto', + prompt: 'Port', value: existing?.port || engine.defaultPort, minimum: 1, }); @@ -271,7 +271,7 @@ async function promptConnection(options = {}) { profile.database = await inputText({ title: `Simple DB — ${engine.displayName}`, - prompt: engineId === 'mysql' ? 'Base de datos inicial (opcional)' : 'Base de datos inicial', + prompt: engineId === 'mysql' ? 'Initial database (optional)' : 'Initial database', value: existing?.database || (engineId === 'postgresql' ? 'postgres' : engineId === 'sqlserver' ? 'master' : ''), @@ -281,7 +281,7 @@ async function promptConnection(options = {}) { profile.user = await inputText({ title: `Simple DB — ${engine.displayName}`, - prompt: 'Usuario', + prompt: 'Username', value: existing?.user || '', required: true, }); @@ -289,7 +289,7 @@ async function promptConnection(options = {}) { password = await inputText({ title: `Simple DB — ${engine.displayName}`, - prompt: existing ? 'Contraseña (vacío = conservar la actual)' : 'Contraseña', + prompt: existing ? 'Password (leave empty to keep the current one)' : 'Password', password: true, required: false, }); @@ -298,27 +298,27 @@ async function promptConnection(options = {}) { if (engineId === 'sqlserver') { profile.encrypt = await inputBoolean( 'Simple DB — SQL Server', - '¿Cifrar la conexión?', + 'Encrypt the connection?', existing?.encrypt !== false, ); if (profile.encrypt === undefined) return null; profile.trustServerCertificate = await inputBoolean( 'Simple DB — SQL Server', - '¿Confiar en el certificado del servidor?', + 'Trust the server certificate?', existing?.trustServerCertificate || false, ); if (profile.trustServerCertificate === undefined) return null; } else { profile.ssl = await inputBoolean( `Simple DB — ${engine.displayName}`, - '¿Usar SSL/TLS?', + 'Use SSL/TLS?', existing?.ssl || false, ); if (profile.ssl === undefined) return null; if (profile.ssl) { profile.trustServerCertificate = await inputBoolean( `Simple DB — ${engine.displayName}`, - '¿Aceptar un certificado no verificado?', + 'Accept an unverified certificate?', existing?.trustServerCertificate || false, ); if (profile.trustServerCertificate === undefined) return null; @@ -330,7 +330,7 @@ async function promptConnection(options = {}) { profile.connectTimeoutMs = await inputNumber({ title: `Simple DB — ${engine.displayName}`, - prompt: 'Tiempo máximo de conexión (ms)', + prompt: 'Connection timeout (ms)', value: existing?.connectTimeoutMs ?? 15000, minimum: 1, }); @@ -338,7 +338,7 @@ async function promptConnection(options = {}) { profile.queryTimeoutMs = await inputNumber({ title: `Simple DB — ${engine.displayName}`, - prompt: 'Tiempo máximo por consulta (ms; 0 = sin límite)', + prompt: 'Query timeout (ms; 0 = no timeout)', value: existing?.queryTimeoutMs ?? 300000, minimum: 0, }); @@ -347,18 +347,18 @@ async function promptConnection(options = {}) { const finish = await vscode.window.showQuickPick( [ { - label: '$(beaker) Probar conexión y guardar', + label: '$(beaker) Test Connection and Save', value: 'test', - description: 'Recomendado', + description: 'Recommended', }, { - label: '$(save) Guardar sin probar', + label: '$(save) Save without Testing', value: 'save', }, ], { title: `Simple DB — ${profile.name}`, - placeHolder: '¿Cómo quieres terminar?', + placeHolder: 'How do you want to finish?', ignoreFocusOut: true, }, ); diff --git a/src/views/connectionsTreeProvider.js b/src/views/connectionsTreeProvider.js index cfd9613..dafcf3a 100644 --- a/src/views/connectionsTreeProvider.js +++ b/src/views/connectionsTreeProvider.js @@ -34,17 +34,17 @@ class ConnectionsTreeProvider { ); item.id = `simpleDb.engine.${node.engineId}`; item.contextValue = 'simpleDb.engine'; - item.description = `${count} conexión${count === 1 ? '' : 'es'}`; + item.description = `${count} connection${count === 1 ? '' : 's'}`; item.iconPath = new vscode.ThemeIcon('server-environment'); item.tooltip = engine.defaultPort - ? `${engine.displayName} — puerto predeterminado ${engine.defaultPort}` - : `${engine.displayName} — base de datos en archivo`; + ? `${engine.displayName} — default port ${engine.defaultPort}` + : `${engine.displayName} — file database`; return item; } case 'connection': { const profile = this.connectionStore.get(node.profileId); if (!profile) { - return new vscode.TreeItem('Conexión eliminada'); + return new vscode.TreeItem('Connection deleted'); } const status = this.connectionManager.status(profile.id); const connected = this.connectionManager.isConnected(profile.id); @@ -62,16 +62,16 @@ class ConnectionsTreeProvider { : 'simpleDb.connection.connected' : 'simpleDb.connection.disconnected'; if (status.state === 'connecting') { - item.description = 'conectando…'; + item.description = 'connecting…'; item.iconPath = new vscode.ThemeIcon('sync~spin'); } else if (status.state === 'error') { item.description = 'error'; item.iconPath = new vscode.ThemeIcon('error'); } else if (connected) { - item.description = transactionCount > 0 ? `TX: ${transactionCount}` : 'conectada'; + item.description = transactionCount > 0 ? `TX: ${transactionCount}` : 'connected'; item.iconPath = new vscode.ThemeIcon('database'); } else { - item.description = 'desconectada'; + item.description = 'disconnected'; item.iconPath = new vscode.ThemeIcon('circle-outline'); } let location; @@ -89,7 +89,7 @@ class ConnectionsTreeProvider { tooltip.appendMarkdown(`${getDatabaseEngine(profile.engine)?.displayName || profile.engine} \n`); tooltip.appendText(location); if (status.serverVersion) { - tooltip.appendMarkdown(` \nServidor: ${status.serverVersion}`); + tooltip.appendMarkdown(` \nServer: ${status.serverVersion}`); } if (status.error) { tooltip.appendMarkdown(` \nError: ${status.error}`); @@ -121,17 +121,17 @@ class ConnectionsTreeProvider { } case 'group': { const labels = { - tables: 'Tablas', - views: 'Vistas', - materializedViews: 'Vistas materializadas', - procedures: 'Procedimientos y funciones', + tables: 'Tables', + views: 'Views', + materializedViews: 'Materialized Views', + procedures: 'Procedures and Functions', packages: 'Packages', - indexes: 'Índices', + indexes: 'Indexes', triggers: 'Triggers', - sequences: 'Secuencias', - types: 'Tipos', - synonyms: 'Sinónimos', - events: 'Eventos', + sequences: 'Sequences', + types: 'Types', + synonyms: 'Synonyms', + events: 'Events', }; const icons = { tables: 'table', @@ -195,7 +195,7 @@ class ConnectionsTreeProvider { case 'message': default: { const item = new vscode.TreeItem( - node.label || 'Sin elementos', + node.label || 'No items', vscode.TreeItemCollapsibleState.None, ); item.contextValue = 'simpleDb.message'; @@ -221,12 +221,12 @@ class ConnectionsTreeProvider { .sort((a, b) => a.name.localeCompare(b.name)); return profiles.length ? profiles.map((profile) => ({ kind: 'connection', profileId: profile.id })) - : [this._message('Sin conexiones configuradas', node.engineId)]; + : [this._message('No connections configured', node.engineId)]; } if (node.kind === 'connection') { if (!this.connectionManager.isConnected(node.profileId)) { - return [this._message('Conecta para explorar la base de datos', node.profileId)]; + return [this._message('Connect to explore the database', node.profileId)]; } const databases = await this.connectionManager.listDatabases(node.profileId); return databases.length @@ -236,7 +236,7 @@ class ConnectionsTreeProvider { database: String(database.name), file: database.file, })) - : [this._message('Sin bases de datos visibles', node.profileId)]; + : [this._message('No visible databases', node.profileId)]; } if (node.kind === 'database') { @@ -255,7 +255,7 @@ class ConnectionsTreeProvider { database: node.database, schema: String(schema.name), })) - : [this._message('Sin esquemas visibles', `${node.profileId}.${node.database}`)]; + : [this._message('No visible schemas', `${node.profileId}.${node.database}`)]; } if (node.kind === 'schema') { @@ -293,7 +293,7 @@ class ConnectionsTreeProvider { name: String(object.name), type: object.type || '', })) - : [this._message('Sin elementos', `${node.profileId}.${node.groupType}`)]; + : [this._message('No items', `${node.profileId}.${node.groupType}`)]; } if ( @@ -314,7 +314,7 @@ class ConnectionsTreeProvider { nullable: column.nullable === true || column.nullable === 1, position: column.position, })) - : [this._message('Sin columnas visibles', `${node.profileId}.${node.name}`)]; + : [this._message('No visible columns', `${node.profileId}.${node.name}`)]; } return []; diff --git a/src/views/historyTreeProvider.js b/src/views/historyTreeProvider.js index 383073f..99d07c7 100644 --- a/src/views/historyTreeProvider.js +++ b/src/views/historyTreeProvider.js @@ -24,23 +24,23 @@ class HistoryTreeProvider { getTreeItem(node) { if (node.kind === 'empty') { - const item = new vscode.TreeItem('Sin consultas en el historial'); + const item = new vscode.TreeItem('No queries in history'); item.iconPath = new vscode.ThemeIcon('info'); return item; } const entry = node.entry; const item = new vscode.TreeItem( - compactSql(entry.sql) || '(consulta vacía)', + compactSql(entry.sql) || '(empty query)', vscode.TreeItemCollapsibleState.None, ); item.contextValue = 'simpleDb.historyEntry'; - const affected = entry.affectedRows ? ` • ${entry.affectedRows} afectadas` : ''; - item.description = `${entry.connectionName} • ${entry.rows} filas${affected} • ${entry.durationMs} ms`; + const affected = entry.affectedRows ? ` • ${entry.affectedRows} affected` : ''; + item.description = `${entry.connectionName} • ${entry.rows} rows${affected} • ${entry.durationMs} ms`; item.iconPath = new vscode.ThemeIcon(entry.success ? 'pass-filled' : 'error'); item.command = { command: 'simpleDb.openHistoryEntry', - title: 'Abrir consulta', + title: 'Open Query', arguments: [node], }; diff --git a/src/views/resultPanel.js b/src/views/resultPanel.js index 19f63b3..599338b 100644 --- a/src/views/resultPanel.js +++ b/src/views/resultPanel.js @@ -24,7 +24,7 @@ class ResultPanel { if (!this.panel) { this.panel = vscode.window.createWebviewPanel( 'simpleDb.results', - 'Simple DB — Resultados', + 'Simple DB — Results', vscode.ViewColumn.Beside, { enableScripts: true, retainContextWhenHidden: true }, ); @@ -64,7 +64,7 @@ class ResultPanel { const filename = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: `Simple DB — Exportando ${format.toUpperCase()}`, + title: `Simple DB — Exporting ${format.toUpperCase()}`, cancellable: false, }, () => @@ -75,7 +75,7 @@ class ResultPanel { ), ); if (filename) { - vscode.window.showInformationMessage(`Simple DB: exportado a ${filename}`); + vscode.window.showInformationMessage(`Simple DB: exported to ${filename}`); } return; } @@ -111,7 +111,7 @@ class ResultPanel { _html(webview, executionId) { const n = nonce(); return ` - + @@ -141,17 +141,17 @@ class ResultPanel { -
Simple DBCargando…
+
Simple DBLoading…
-
Cargando resultados…
+
Loading results…