sqlstack 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/adapters/pg.d.ts.map +1 -1
- package/dist/cjs/adapters/pg.js +13 -9
- package/dist/cjs/adapters/pg.js.map +1 -1
- package/dist/cjs/adapters/sqlite.d.ts.map +1 -1
- package/dist/cjs/adapters/sqlite.js +15 -8
- package/dist/cjs/adapters/sqlite.js.map +1 -1
- package/dist/cjs/core/errors.d.ts +5 -0
- package/dist/cjs/core/errors.d.ts.map +1 -1
- package/dist/cjs/core/errors.js +15 -1
- package/dist/cjs/core/errors.js.map +1 -1
- package/dist/cjs/decorators/query.d.ts.map +1 -1
- package/dist/cjs/decorators/query.js +255 -241
- package/dist/cjs/decorators/query.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/registry.d.ts +3 -4
- package/dist/cjs/registry.d.ts.map +1 -1
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/run.d.ts +29 -0
- package/dist/cjs/run.d.ts.map +1 -0
- package/dist/cjs/run.js +38 -0
- package/dist/cjs/run.js.map +1 -0
- package/dist/cjs/testing/index.d.ts +4 -0
- package/dist/cjs/testing/index.d.ts.map +1 -1
- package/dist/cjs/testing/index.js +5 -1
- package/dist/cjs/testing/index.js.map +1 -1
- package/dist/cjs/testing/schema/ddl.d.ts +1 -10
- package/dist/cjs/testing/schema/ddl.d.ts.map +1 -1
- package/dist/cjs/testing/schema/ddl.js +29 -13
- package/dist/cjs/testing/schema/ddl.js.map +1 -1
- package/dist/cjs/testing/schema/normalize.js +3 -3
- package/dist/cjs/testing/schema/normalize.js.map +1 -1
- package/dist/cjs/testing/schema/types.d.ts +1 -20
- package/dist/cjs/testing/schema/types.d.ts.map +1 -1
- package/dist/cjs/testing/schema/types.js +0 -4
- package/dist/cjs/testing/schema/types.js.map +1 -1
- package/dist/cjs/testing/sqlite/builder.d.ts +24 -0
- package/dist/cjs/testing/sqlite/builder.d.ts.map +1 -0
- package/dist/cjs/testing/sqlite/builder.js +134 -0
- package/dist/cjs/testing/sqlite/builder.js.map +1 -0
- package/dist/cjs/testing/sqlite/database.d.ts +18 -0
- package/dist/cjs/testing/sqlite/database.d.ts.map +1 -0
- package/dist/cjs/testing/sqlite/database.js +123 -0
- package/dist/cjs/testing/sqlite/database.js.map +1 -0
- package/dist/cjs/testing/sqlite/loadData.d.ts +7 -0
- package/dist/cjs/testing/sqlite/loadData.d.ts.map +1 -0
- package/dist/cjs/testing/sqlite/loadData.js +55 -0
- package/dist/cjs/testing/sqlite/loadData.js.map +1 -0
- package/dist/esm/adapters/pg.js +13 -9
- package/dist/esm/adapters/pg.js.map +1 -1
- package/dist/esm/adapters/sqlite.js +15 -8
- package/dist/esm/adapters/sqlite.js.map +1 -1
- package/dist/esm/core/errors.js +13 -0
- package/dist/esm/core/errors.js.map +1 -1
- package/dist/esm/decorators/query.js +256 -242
- package/dist/esm/decorators/query.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/registry.js.map +1 -1
- package/dist/esm/run.js +34 -0
- package/dist/esm/run.js.map +1 -0
- package/dist/esm/testing/index.js +2 -0
- package/dist/esm/testing/index.js.map +1 -1
- package/dist/esm/testing/schema/ddl.js +29 -13
- package/dist/esm/testing/schema/ddl.js.map +1 -1
- package/dist/esm/testing/schema/normalize.js +3 -3
- package/dist/esm/testing/schema/normalize.js.map +1 -1
- package/dist/esm/testing/schema/types.js +0 -4
- package/dist/esm/testing/schema/types.js.map +1 -1
- package/dist/esm/testing/sqlite/builder.js +98 -0
- package/dist/esm/testing/sqlite/builder.js.map +1 -0
- package/dist/esm/testing/sqlite/database.js +85 -0
- package/dist/esm/testing/sqlite/database.js.map +1 -0
- package/dist/esm/testing/sqlite/loadData.js +48 -0
- package/dist/esm/testing/sqlite/loadData.js.map +1 -0
- package/package.json +4 -3
- package/readme.md +6 -8
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { createSqliteDb } from '../../adapters/sqlite.js';
|
|
5
|
+
import { SqlStackError } from '../../core/errors.js';
|
|
6
|
+
export class TestSqliteError extends SqlStackError {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'TestSqliteError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Temp directories for file-backed handles that were never disposed. Tests are
|
|
14
|
+
* expected to call `dispose()`, but a failed assertion can skip it; this keeps
|
|
15
|
+
* an abandoned directory from outliving the worker process.
|
|
16
|
+
*/
|
|
17
|
+
const abandonedDirectories = new Set();
|
|
18
|
+
let exitCleanupRegistered = false;
|
|
19
|
+
function trackDirectory(directory) {
|
|
20
|
+
abandonedDirectories.add(directory);
|
|
21
|
+
if (exitCleanupRegistered)
|
|
22
|
+
return;
|
|
23
|
+
exitCleanupRegistered = true;
|
|
24
|
+
process.once('exit', () => {
|
|
25
|
+
for (const dir of abandonedDirectories) {
|
|
26
|
+
try {
|
|
27
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* best-effort cleanup */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function createDatabaseHandle(fileBacked) {
|
|
36
|
+
let directory;
|
|
37
|
+
let filename = ':memory:';
|
|
38
|
+
if (fileBacked) {
|
|
39
|
+
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlstack-test-'));
|
|
40
|
+
filename = path.join(directory, 'database.sqlite');
|
|
41
|
+
trackDirectory(directory);
|
|
42
|
+
}
|
|
43
|
+
let db;
|
|
44
|
+
try {
|
|
45
|
+
db = await createSqliteDb(filename);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (directory) {
|
|
49
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
50
|
+
abandonedDirectories.delete(directory);
|
|
51
|
+
}
|
|
52
|
+
const message = err.message;
|
|
53
|
+
if (message.includes('No SQLite driver available')) {
|
|
54
|
+
throw new TestSqliteError('testSqlite requires the optional peer "better-sqlite3".');
|
|
55
|
+
}
|
|
56
|
+
throw new TestSqliteError(`Failed to create SQLite test database: ${message}`);
|
|
57
|
+
}
|
|
58
|
+
let disposed = false;
|
|
59
|
+
return {
|
|
60
|
+
db,
|
|
61
|
+
conn: db.conn,
|
|
62
|
+
path: fileBacked ? filename : undefined,
|
|
63
|
+
async query(sql, params = []) {
|
|
64
|
+
if (disposed)
|
|
65
|
+
throw new TestSqliteError('Test SQLite database has been disposed.');
|
|
66
|
+
return db.query(sql, [...params]);
|
|
67
|
+
},
|
|
68
|
+
async dispose() {
|
|
69
|
+
if (disposed)
|
|
70
|
+
return;
|
|
71
|
+
disposed = true;
|
|
72
|
+
try {
|
|
73
|
+
await db.close();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* connection may already be closed */
|
|
77
|
+
}
|
|
78
|
+
if (directory) {
|
|
79
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
80
|
+
abandonedDirectories.delete(directory);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../../../src/testing/sqlite/database.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { createSqliteDb } from '../../adapters/sqlite';\nimport { SqlStackError } from '../../core/errors';\nimport type { Database } from '../../registry';\n\nexport class TestSqliteError extends SqlStackError {\n constructor(message: string) {\n super(message);\n this.name = 'TestSqliteError';\n }\n}\n\nexport interface TestSqliteDatabase {\n /** The live sqlstack SQLite adapter, suitable for direct injection. */\n readonly db: Database;\n /** The underlying better-sqlite3 (or sqlite3) connection. */\n readonly conn: unknown;\n /** Absolute database path in file mode; undefined for :memory:. */\n readonly path?: string;\n query(sql: string, params?: readonly unknown[]): Promise<unknown>;\n /** Close the connection and remove owned temporary files. Idempotent. */\n dispose(): Promise<void>;\n}\n\n/**\n * Temp directories for file-backed handles that were never disposed. Tests are\n * expected to call `dispose()`, but a failed assertion can skip it; this keeps\n * an abandoned directory from outliving the worker process.\n */\nconst abandonedDirectories = new Set<string>();\nlet exitCleanupRegistered = false;\n\nfunction trackDirectory(directory: string): void {\n abandonedDirectories.add(directory);\n if (exitCleanupRegistered) return;\n exitCleanupRegistered = true;\n process.once('exit', () => {\n for (const dir of abandonedDirectories) {\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n } catch {\n /* best-effort cleanup */\n }\n }\n });\n}\n\nexport async function createDatabaseHandle(\n fileBacked: boolean\n): Promise<TestSqliteDatabase> {\n let directory: string | undefined;\n let filename = ':memory:';\n if (fileBacked) {\n directory = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlstack-test-'));\n filename = path.join(directory, 'database.sqlite');\n trackDirectory(directory);\n }\n\n let db: Database;\n try {\n db = await createSqliteDb(filename);\n } catch (err) {\n if (directory) {\n fs.rmSync(directory, { recursive: true, force: true });\n abandonedDirectories.delete(directory);\n }\n const message = (err as Error).message;\n if (message.includes('No SQLite driver available')) {\n throw new TestSqliteError('testSqlite requires the optional peer \"better-sqlite3\".');\n }\n throw new TestSqliteError(`Failed to create SQLite test database: ${message}`);\n }\n\n let disposed = false;\n return {\n db,\n conn: db.conn,\n path: fileBacked ? filename : undefined,\n async query(sql: string, params: readonly unknown[] = []) {\n if (disposed) throw new TestSqliteError('Test SQLite database has been disposed.');\n return db.query(sql, [...params]);\n },\n async dispose() {\n if (disposed) return;\n disposed = true;\n try {\n await db.close();\n } catch {\n /* connection may already be closed */\n }\n if (directory) {\n fs.rmSync(directory, { recursive: true, force: true });\n abandonedDirectories.delete(directory);\n }\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { quoteIdent } from '../schema/ddl.js';
|
|
2
|
+
import { TestDataError, composeRows, planInsertOrder, toOperations, } from '../postgres/loadData.js';
|
|
3
|
+
export { TestDataError, composeRows, planInsertOrder, toOperations };
|
|
4
|
+
export async function insertRows(query, schema, effective) {
|
|
5
|
+
const order = planInsertOrder(schema, [...effective.keys()]);
|
|
6
|
+
const declared = new Map(schema.tables.map((table) => [
|
|
7
|
+
table.name,
|
|
8
|
+
table.columns.map((column) => column.name),
|
|
9
|
+
]));
|
|
10
|
+
for (const tableName of order) {
|
|
11
|
+
const rows = effective.get(tableName);
|
|
12
|
+
const columnOrder = declared.get(tableName);
|
|
13
|
+
const batches = new Map();
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
const columns = columnOrder.filter((column) => row[column] !== undefined);
|
|
16
|
+
const signature = columns.join(',');
|
|
17
|
+
const batch = batches.get(signature) ?? { columns, rows: [] };
|
|
18
|
+
batch.rows.push(row);
|
|
19
|
+
batches.set(signature, batch);
|
|
20
|
+
}
|
|
21
|
+
for (const { columns, rows: batchRows } of batches.values()) {
|
|
22
|
+
if (columns.length === 0) {
|
|
23
|
+
for (let i = 0; i < batchRows.length; i++) {
|
|
24
|
+
await run(query, tableName, `INSERT INTO ${quoteIdent(tableName)} DEFAULT VALUES`, []);
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const params = [];
|
|
29
|
+
const tuples = batchRows.map((row) => {
|
|
30
|
+
const placeholders = columns.map((column) => {
|
|
31
|
+
params.push(row[column]);
|
|
32
|
+
return '?';
|
|
33
|
+
});
|
|
34
|
+
return `(${placeholders.join(', ')})`;
|
|
35
|
+
});
|
|
36
|
+
await run(query, tableName, `INSERT INTO ${quoteIdent(tableName)} (${columns.map(quoteIdent).join(', ')}) VALUES ${tuples.join(', ')}`, params);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function run(query, tableName, sql, params) {
|
|
41
|
+
try {
|
|
42
|
+
await query(sql, params);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw new TestDataError(`Failed to insert scenario rows into table "${tableName}": ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=loadData.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadData.js","sourceRoot":"","sources":["../../../../src/testing/sqlite/loadData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EACL,aAAa,EACb,WAAW,EACX,eAAe,EACf,YAAY,GACb,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAGrE,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAA2D,EAC3D,MAA4B,EAC5B,SAA6B;IAE7B,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QACpD,KAAK,CAAC,IAAI;QACV,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;KAC3C,CAAC,CAAC,CAAC;IACJ,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC;QACvC,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8C,CAAC;QACtE,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC;YAC1E,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;YAC9D,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACrB,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;QACD,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC1C,MAAM,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,eAAe,UAAU,CAAC,SAAS,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;gBACzF,CAAC;gBACD,SAAS;YACX,CAAC;YACD,MAAM,MAAM,GAAc,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;gBACnC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;oBAC1C,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;oBACzB,OAAO,GAAG,CAAC;gBACb,CAAC,CAAC,CAAC;gBACH,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YACxC,CAAC,CAAC,CAAC;YACH,MAAM,GAAG,CACP,KAAK,EACL,SAAS,EACT,eAAe,UAAU,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAC1G,MAAM,CACP,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,GAAG,CAChB,KAA2D,EAC3D,SAAiB,EACjB,GAAW,EACX,MAAiB;IAEjB,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,aAAa,CACrB,8CAA8C,SAAS,MAAO,GAAa,CAAC,OAAO,EAAE,CACtF,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["import { quoteIdent } from '../schema/ddl';\nimport type { NormalizedTestSchema } from '../schema/types';\nimport {\n TestDataError,\n composeRows,\n planInsertOrder,\n toOperations,\n} from '../postgres/loadData';\nimport type { DataOperation, Row } from '../postgres/loadData';\n\nexport { TestDataError, composeRows, planInsertOrder, toOperations };\nexport type { DataOperation, Row };\n\nexport async function insertRows(\n query: (sql: string, params: unknown[]) => Promise<unknown>,\n schema: NormalizedTestSchema,\n effective: Map<string, Row[]>\n): Promise<void> {\n const order = planInsertOrder(schema, [...effective.keys()]);\n const declared = new Map(schema.tables.map((table) => [\n table.name,\n table.columns.map((column) => column.name),\n ]));\n for (const tableName of order) {\n const rows = effective.get(tableName)!;\n const columnOrder = declared.get(tableName)!;\n const batches = new Map<string, { columns: string[]; rows: Row[] }>();\n for (const row of rows) {\n const columns = columnOrder.filter((column) => row[column] !== undefined);\n const signature = columns.join(',');\n const batch = batches.get(signature) ?? { columns, rows: [] };\n batch.rows.push(row);\n batches.set(signature, batch);\n }\n for (const { columns, rows: batchRows } of batches.values()) {\n if (columns.length === 0) {\n for (let i = 0; i < batchRows.length; i++) {\n await run(query, tableName, `INSERT INTO ${quoteIdent(tableName)} DEFAULT VALUES`, []);\n }\n continue;\n }\n const params: unknown[] = [];\n const tuples = batchRows.map((row) => {\n const placeholders = columns.map((column) => {\n params.push(row[column]);\n return '?';\n });\n return `(${placeholders.join(', ')})`;\n });\n await run(\n query,\n tableName,\n `INSERT INTO ${quoteIdent(tableName)} (${columns.map(quoteIdent).join(', ')}) VALUES ${tuples.join(', ')}`,\n params\n );\n }\n }\n}\n\nasync function run(\n query: (sql: string, params: unknown[]) => Promise<unknown>,\n tableName: string,\n sql: string,\n params: unknown[]\n): Promise<void> {\n try {\n await query(sql, params);\n } catch (err) {\n throw new TestDataError(\n `Failed to insert scenario rows into table \"${tableName}\": ${(err as Error).message}`\n );\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqlstack",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"build:cjs": "tsc -p tsconfig.json",
|
|
57
57
|
"build:esm": "tsc -p tsconfig.esm.json && node scripts/fix-esm-extensions.js && node scripts/create-esm-package.cjs",
|
|
58
58
|
"test": "jest",
|
|
59
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
60
|
"test:watch": "jest --watch",
|
|
60
61
|
"clean": "rimraf dist",
|
|
61
62
|
"test:integration": "jest -c jest.integration.config.ts",
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
"db:logs:mysql": "docker logs -f sqlstack-mysql"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
|
-
"@noego/ioc": "
|
|
74
|
+
"@noego/ioc": "^0.6.1",
|
|
74
75
|
"@types/jest": "^29.5.13",
|
|
75
76
|
"better-sqlite3": "^12.3.0",
|
|
76
77
|
"jest": "^29.7.0",
|
|
@@ -88,7 +89,7 @@
|
|
|
88
89
|
"yaml": "^2.9.0"
|
|
89
90
|
},
|
|
90
91
|
"peerDependencies": {
|
|
91
|
-
"@noego/ioc": "
|
|
92
|
+
"@noego/ioc": ">=0.6.1",
|
|
92
93
|
"better-sqlite3": "*",
|
|
93
94
|
"mysql2": "*",
|
|
94
95
|
"pg": "*",
|
package/readme.md
CHANGED
|
@@ -828,16 +828,14 @@ const result: WriteResult = await db.query(
|
|
|
828
828
|
console.log(result.rowsAffected); // number
|
|
829
829
|
```
|
|
830
830
|
|
|
831
|
-
**Postgres
|
|
831
|
+
**With RETURNING (Postgres, SQLite ≥ 3.35):** the statement produces a result set, so it returns rows exactly like a `SELECT` — and `@Single` / `@Only` / `@Exist` / `@None` apply to it. The affected count is `rows.length`.
|
|
832
832
|
|
|
833
833
|
```ts
|
|
834
|
-
const
|
|
834
|
+
const rows = await db.query(
|
|
835
835
|
'INSERT INTO users(id, email) VALUES($1, $2) RETURNING id',
|
|
836
836
|
['u9', 'nine@example.com']
|
|
837
837
|
);
|
|
838
|
-
|
|
839
|
-
console.log(pgResult.returning[0].id); // 'u9'
|
|
840
|
-
}
|
|
838
|
+
console.log(rows[0].id); // 'u9'
|
|
841
839
|
```
|
|
842
840
|
|
|
843
841
|
Type definition:
|
|
@@ -847,11 +845,11 @@ type WriteResult = {
|
|
|
847
845
|
lastInsertId?: unknown;
|
|
848
846
|
};
|
|
849
847
|
|
|
850
|
-
type
|
|
851
|
-
returning?: Row[];
|
|
852
|
-
};
|
|
848
|
+
type QueryResult = Row[] | WriteResult;
|
|
853
849
|
```
|
|
854
850
|
|
|
851
|
+
A result-shape decorator on a method whose statement produces no result set (e.g. `@Single` on a plain `UPDATE`) throws `ExpectedRowsError` rather than returning the write result.
|
|
852
|
+
|
|
855
853
|
### Evolving Queries Without Breaking Callers
|
|
856
854
|
|
|
857
855
|
Your method signature is the contract; the SQL is an implementation detail.
|