Skip to content

Commit 70e1c3a

Browse files
committed
test: cover ODBC adapter and JOIN predicates for Sonar
1 parent ace48a3 commit 70e1c3a

7 files changed

Lines changed: 136 additions & 3 deletions

File tree

docs/releases/v0.2.11.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
- **autocomplete:** suggest foreign key predicates at the start of a JOIN `ON` clause, using the aliases in the query.
66
- **results:** navigate from foreign key values to the referenced row.
7+
- **editor:** open a searchable SQL command catalog with `/catalog` and insert a command template.
78

89
### Bug Fixes
910

1011
- **updater:** stop bundled runtimes before installing Windows updates.
12+
- **editor:** explain the current statement or selection and recognize pasted Unicode whitespace.
13+
- **sidebar:** align object rows and tighten spacing.
1114

1215
**Full Changelog:** [`v0.2.10...v0.2.11`](https://github.com/cccadet/omni-sql/compare/v0.2.10...v0.2.11)

packages/adapters-odbc/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"typecheck": "tsc --noEmit",
1111
"lint": "eslint src",
1212
"test": "node --test ./src/index.test.ts",
13+
"coverage": "mkdir -p coverage && node --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-coverage-include='src/**/*.ts' --test-coverage-exclude='**/*.test.ts' --test ./src/index.test.ts",
1314
"test:integration": "node --test ./src/integration.test.ts"
1415
},
1516
"dependencies": {
Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,85 @@
11
import assert from "node:assert/strict";
22
import { test } from "node:test";
3-
import { buildOdbcConnectionString } from "./index.ts";
3+
import type { ConnectionConfig } from "@omni-sql/ts-types";
4+
import { OdbcAdapter, buildOdbcConnectionString } from "./index.ts";
5+
6+
const config: ConnectionConfig = {
7+
id: "odbc-test", label: "Mock ODBC", dialect: "odbc", endpoint: "Test DSN", user: "alice",
8+
};
49

510
test("builds DSN and DSN-less connection strings without persisting credentials", () => {
611
assert.equal(buildOdbcConnectionString("Corporate DB", "alice", "s}ecret"), "DSN={Corporate DB};UID={alice};PWD={s}}ecret}");
712
assert.equal(buildOdbcConnectionString("DRIVER={SQLite3};Database=C:\\db.sqlite;", "", undefined), "DRIVER={SQLite3};Database=C:\\db.sqlite");
813
});
14+
15+
test("ODBC introspects tables, views, columns, and primary keys through the driver", async () => {
16+
let closeCount = 0;
17+
const connection = {
18+
close: async () => { closeCount++; },
19+
tables: async () => [
20+
{ TABLE_CAT: "catalog", TABLE_SCHEM: "public", TABLE_NAME: "orders", TABLE_TYPE: "TABLE", REMARKS: " Orders " },
21+
{ TABLE_CAT: "catalog", TABLE_SCHEM: "public", TABLE_NAME: "orders", TABLE_TYPE: "TABLE" },
22+
{ TABLE_CAT: "catalog", TABLE_SCHEM: "public", TABLE_NAME: "order_view", TABLE_TYPE: "VIEW" },
23+
{ TABLE_NAME: "" },
24+
],
25+
columns: async (_catalog: unknown, _schema: unknown, table: string) => table === "orders"
26+
? [
27+
{ COLUMN_NAME: "name", TYPE_NAME: "VARCHAR", NULLABLE: 1, ORDINAL_POSITION: 2, REMARKS: " Label " },
28+
{ COLUMN_NAME: "id", TYPE_NAME: "INTEGER", NULLABLE: 0, ORDINAL_POSITION: 1 },
29+
]
30+
: [],
31+
primaryKeys: async (_catalog: unknown, _schema: unknown, table: string) => table === "orders"
32+
? [{ COLUMN_NAME: "id" }] : [],
33+
};
34+
const driver = { connect: async () => connection } as unknown as ConstructorParameters<typeof OdbcAdapter>[2];
35+
const adapter = new OdbcAdapter(config, "secret", driver);
36+
assert.deepEqual(await adapter.listAvailableSchemas(), ["public"]);
37+
await adapter.introspect();
38+
assert.deepEqual(adapter.listTables("public").map((table) => [table.name, table.kind]), [
39+
["orders", "table"], ["order_view", "view"],
40+
]);
41+
const columns = adapter.listColumns("public", "orders");
42+
assert.deepEqual(columns.map((column) => column.name), ["id", "name"]);
43+
assert.equal(columns[0]?.isPrimaryKey, true);
44+
assert.equal(columns[1]?.description, "Label");
45+
await adapter.close();
46+
assert.equal(closeCount, 1);
47+
});
48+
49+
test("ODBC returns bounded query rows and closes its cursor", async () => {
50+
let closed = false;
51+
const result = Object.assign([
52+
{ id: 1, amount: 12n, created: new Date("2026-09-16T00:00:00.000Z") },
53+
{ id: 2, amount: 13n, created: new Date("2026-09-17T00:00:00.000Z") },
54+
], {
55+
columns: [
56+
{ name: "id", dataTypeName: "INTEGER", dataType: 4, nullable: false },
57+
{ name: "amount", dataTypeName: "BIGINT", dataType: -5, nullable: false },
58+
{ name: "created", dataTypeName: "TIMESTAMP", dataType: 93, nullable: false },
59+
],
60+
count: -1,
61+
});
62+
const connection = {
63+
close: async () => undefined,
64+
query: async () => ({ fetch: async () => result, close: async () => { closed = true; }, noData: false }),
65+
};
66+
const driver = { connect: async () => connection } as unknown as ConstructorParameters<typeof OdbcAdapter>[2];
67+
const adapter = new OdbcAdapter(config, undefined, driver);
68+
const query = await adapter.runQuery("SELECT * FROM orders", 1);
69+
assert.deepEqual(query.rows, [[1, "12", "2026-09-16T00:00:00.000Z"]]);
70+
assert.equal(query.rowsMoreAvailable, true);
71+
assert.equal(query.rowsAffected, undefined);
72+
assert.equal(closed, true);
73+
await adapter.close();
74+
});
75+
76+
test("ODBC classifies driver errors without exposing passwords", async () => {
77+
const driver = { connect: async () => { throw new Error("IM002 driver not found;PWD=secret"); } } as unknown as ConstructorParameters<typeof OdbcAdapter>[2];
78+
const adapter = new OdbcAdapter(config, "secret", driver);
79+
const status = await adapter.test();
80+
assert.equal(status.ok, false);
81+
assert.match(status.message ?? "", /PWD=\*\*\*/u);
82+
assert.doesNotMatch(status.message ?? "", /secret/u);
83+
await assert.rejects(adapter.connect(), (error: unknown) =>
84+
error instanceof Error && "causeTag" in error && error.causeTag === "driver-missing");
85+
});

packages/autocomplete-engine/src/engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ function joinForeignKeySuggestions(ctx: ResolvedContext, meta: MetadataSource):
380380
...(partial ? { filterText: partial } : {}), relevance: 1000 });
381381
}
382382
};
383-
for (const entry of left) {
383+
for (const entry of left.reverse()) {
384384
add(rightRef, right, entry.ref, entry.relation);
385385
add(entry.ref, entry.relation, rightRef, right);
386386
}

packages/autocomplete-engine/src/index.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,57 @@ test("ON prioriza predicado da FK com aliases e aceita FK nos dois sentidos", ()
357357
.some((suggestion) => suggestion.detail === "chave estrangeira"));
358358
});
359359

360+
test("ON usa a FK da relação recém-joinada contra qualquer relação anterior", () => {
361+
const base = metaOf(postgresDescriptor);
362+
const payments: Relation = {
363+
schema: "public", name: "payments", kind: "table",
364+
columns: [
365+
{ name: "id", dataType: "integer", nullable: false, isPrimaryKey: true, ordinalPosition: 0 },
366+
{ name: "order_id", dataType: "integer", nullable: false, isPrimaryKey: false, ordinalPosition: 1,
367+
foreignKeyTo: { schema: "public", table: "orders", column: "id" } },
368+
{ name: "user_id", dataType: "integer", nullable: false, isPrimaryKey: false, ordinalPosition: 2,
369+
foreignKeyTo: { schema: "public", table: "users", column: "id" } },
370+
],
371+
constraints: [],
372+
};
373+
const meta: MetadataSource = {
374+
...base,
375+
resolveRelation: (ref) => ref.table === "payments" ? payments : base.resolveRelation(ref),
376+
};
377+
const sql = "SELECT * FROM users u JOIN orders o ON true JOIN payments p ON ";
378+
const suggestions = autocompleteTier1(sql, sql.length, meta);
379+
assert.deepEqual(suggestions.slice(0, 2).map((suggestion) => suggestion.label), [
380+
"p.order_id = o.id", "p.user_id = u.id",
381+
]);
382+
assert.ok(suggestions.slice(0, 2).every((suggestion) => suggestion.detail === "chave estrangeira"));
383+
});
384+
385+
test("ON respeita identificadores citados, schema e prefixo parcial", () => {
386+
const base = metaOf(postgresDescriptor);
387+
const orders: Relation = {
388+
...ORDERS,
389+
columns: ORDERS.columns.map((column) => column.name === "user_id"
390+
? { ...column, foreignKeyTo: { schema: "public", table: "users", column: "id" } }
391+
: column),
392+
};
393+
const meta: MetadataSource = {
394+
...base,
395+
resolveRelation: (ref) => ref.table === "orders" ? orders : base.resolveRelation(ref),
396+
};
397+
const sql = 'SELECT * FROM public.users "User Alias" JOIN public.orders o ON user';
398+
const suggestions = autocompleteTier1(sql, sql.length, meta);
399+
assert.equal(suggestions[0]?.insertText, 'o.user_id = "User Alias".id');
400+
assert.equal(suggestions[0]?.filterText, "user");
401+
402+
const otherSchema: MetadataSource = {
403+
...meta,
404+
resolveRelation: (ref) => ref.table === "users" ? { ...USERS, schema: "audit" } : meta.resolveRelation(ref),
405+
};
406+
const withoutMatch = "SELECT * FROM audit.users u JOIN public.orders o ON ";
407+
assert.ok(!autocompleteTier1(withoutMatch, withoutMatch.length, otherSchema)
408+
.some((suggestion) => suggestion.detail === "chave estrangeira"));
409+
});
410+
360411
test("após relação JOIN sugere ON prioritário, sem transições gerais", () => {
361412
const sql = "SELECT * FROM users JOIN orders ";
362413
const out = autocompleteTier1(sql, sql.length, metaOf(postgresDescriptor));

scripts/test-coverage.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const reports = [
1212
["packages/adapters-mssql", "coverage/lcov.info"],
1313
["packages/adapters-oracle", "coverage/lcov.info"],
1414
["packages/adapters-jdbc", "coverage/lcov.info"],
15+
["packages/adapters-odbc", "coverage/lcov.info"],
1516
["packages/autocomplete-engine", "coverage/lcov.info"],
1617
["packages/dialect-descriptors", "coverage/lcov.info"],
1718
["packages/metadata-cache", "coverage/lcov.info"],

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ sonar.exclusions=**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/.tu
1818
# monorepo so the scanner bridge cannot become unresponsive under CI load.
1919
sonar.javascript.node.maxspace=4096
2020

21-
sonar.javascript.lcov.reportPaths=apps/desktop/coverage/lcov.info,packages/adapters-pg/coverage/lcov.info,packages/adapters-mysql/coverage/lcov.info,packages/adapters-mssql/coverage/lcov.info,packages/adapters-oracle/coverage/lcov.info,packages/adapters-jdbc/coverage/lcov.info,packages/autocomplete-engine/coverage/lcov.info,packages/dialect-descriptors/coverage/lcov.info,packages/metadata-cache/coverage/lcov.info,packages/backend/coverage/lcov.info,packages/mcp-server/coverage/lcov.info,packages/ts-types/coverage/lcov.info
21+
sonar.javascript.lcov.reportPaths=apps/desktop/coverage/lcov.info,packages/adapters-pg/coverage/lcov.info,packages/adapters-mysql/coverage/lcov.info,packages/adapters-mssql/coverage/lcov.info,packages/adapters-oracle/coverage/lcov.info,packages/adapters-jdbc/coverage/lcov.info,packages/adapters-odbc/coverage/lcov.info,packages/autocomplete-engine/coverage/lcov.info,packages/dialect-descriptors/coverage/lcov.info,packages/metadata-cache/coverage/lcov.info,packages/backend/coverage/lcov.info,packages/mcp-server/coverage/lcov.info,packages/ts-types/coverage/lcov.info
2222
sonar.coverage.jacoco.xmlReportPaths=services/jvm-sidecar/build/reports/jacoco/test/jacocoTestReport.xml
2323
sonar.rust.lcov.reportPaths=apps/desktop/src-tauri/coverage/lcov.info

0 commit comments

Comments
 (0)