@telorun/sql 0.12.0 → 0.21.1
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/README.md
CHANGED
|
@@ -23,9 +23,9 @@ Built to be language-agnostic and infinitely extensible.
|
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# Reconcile your manifest into a running backend
|
|
26
|
-
$ telo ./examples/
|
|
26
|
+
$ telo ./examples/todo-app
|
|
27
27
|
|
|
28
|
-
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:
|
|
28
|
+
{"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8077"}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Why use Telo?
|
|
@@ -20,6 +20,18 @@ export declare abstract class SqlConnectionBase implements SqlConnection {
|
|
|
20
20
|
teardown(): Promise<void>;
|
|
21
21
|
runInTransaction<T>(body: (bind: (entry: ZoneEntry) => void) => Promise<T>): Promise<T>;
|
|
22
22
|
hasOpenTransaction(ctx?: InvokeContext): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Every statement this connection runs funnels through here — `executeTemplate`
|
|
25
|
+
* and `executeScript` both delegate — so it is the single instrumentation point.
|
|
26
|
+
*
|
|
27
|
+
* `db.query.text` is the statement, never the parameters: the values ARE the
|
|
28
|
+
* data, and a record carrying them would put row contents in the log. The
|
|
29
|
+
* statement itself is safe for a parameterized query (it is the template), and
|
|
30
|
+
* is `debug`-only regardless, because `Sql.Command` can carry inline literals.
|
|
31
|
+
*
|
|
32
|
+
* The disabled path allocates nothing and takes no clock reading — a query is
|
|
33
|
+
* the hottest thing this module does.
|
|
34
|
+
*/
|
|
23
35
|
execute<T>(sql: string, params?: unknown[], zone?: ZoneEntry, ctx?: InvokeContext): Promise<QueryResult<T>>;
|
|
24
36
|
executeTemplate<T>(fragments: string[], values: unknown[], zone?: ZoneEntry, ctx?: InvokeContext): Promise<QueryResult<T>>;
|
|
25
37
|
/** Hand the whole script to the driver as one statement. Backends whose driver
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvokeError, } from "@telorun/sdk";
|
|
1
|
+
import { InvokeError, SEVERITY, } from "@telorun/sdk";
|
|
2
2
|
import { CompiledQuery } from "kysely";
|
|
3
3
|
/**
|
|
4
4
|
* The dialect-neutral half of a connection: statement execution, transaction
|
|
@@ -35,16 +35,55 @@ export class SqlConnectionBase {
|
|
|
35
35
|
await this.db.destroy();
|
|
36
36
|
}
|
|
37
37
|
async runInTransaction(body) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
this.ctx.log.debug("Transaction started");
|
|
39
|
+
try {
|
|
40
|
+
const result = await this.db
|
|
41
|
+
.transaction()
|
|
42
|
+
.execute((trx) => body((entry) => this.#executors.set(entry, trx)));
|
|
43
|
+
this.ctx.log.debug("Transaction committed");
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
// The error reaches the caller, but the ROLLBACK does not: a caller that
|
|
48
|
+
// maps the failure to a response sees nothing saying its writes were
|
|
49
|
+
// discarded, and that is the fact worth reconstructing afterwards.
|
|
50
|
+
this.ctx.log.debug("Transaction rolled back", undefined, { error: err });
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
41
53
|
}
|
|
42
54
|
hasOpenTransaction(ctx) {
|
|
43
55
|
return this.ctx.zonesFor(this, ctx).some((entry) => this.#executors.has(entry));
|
|
44
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Every statement this connection runs funnels through here — `executeTemplate`
|
|
59
|
+
* and `executeScript` both delegate — so it is the single instrumentation point.
|
|
60
|
+
*
|
|
61
|
+
* `db.query.text` is the statement, never the parameters: the values ARE the
|
|
62
|
+
* data, and a record carrying them would put row contents in the log. The
|
|
63
|
+
* statement itself is safe for a parameterized query (it is the template), and
|
|
64
|
+
* is `debug`-only regardless, because `Sql.Command` can carry inline literals.
|
|
65
|
+
*
|
|
66
|
+
* The disabled path allocates nothing and takes no clock reading — a query is
|
|
67
|
+
* the hottest thing this module does.
|
|
68
|
+
*/
|
|
45
69
|
async execute(sql, params = [], zone, ctx) {
|
|
46
70
|
const executor = this.resolveExecutor(zone, ctx);
|
|
47
|
-
|
|
71
|
+
if (!this.ctx.log.enabled(SEVERITY.debug)) {
|
|
72
|
+
return executor.executeQuery(CompiledQuery.raw(sql, params));
|
|
73
|
+
}
|
|
74
|
+
const startedAt = Date.now();
|
|
75
|
+
const result = await executor.executeQuery(CompiledQuery.raw(sql, params));
|
|
76
|
+
this.ctx.log.debug("Statement executed", {
|
|
77
|
+
"db.query.text": sql,
|
|
78
|
+
"db.response.returned_rows": result.rows.length,
|
|
79
|
+
// OTel's own name for this quantity, in OTel's own unit: SECONDS, as a
|
|
80
|
+
// double. Metric names and attribute keys are separate namespaces, so
|
|
81
|
+
// reusing the name is safe — what would not be safe is the name with the
|
|
82
|
+
// wrong magnitude, which is why this is not milliseconds. Units live in
|
|
83
|
+
// the convention, never in the key.
|
|
84
|
+
"db.client.operation.duration": (Date.now() - startedAt) / 1000,
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
48
87
|
}
|
|
49
88
|
async executeTemplate(fragments, values, zone, ctx) {
|
|
50
89
|
let sql = fragments[0] ?? "";
|
|
@@ -61,10 +61,32 @@ class SqlMigrationsResource {
|
|
|
61
61
|
migrationTableName: "migrations",
|
|
62
62
|
migrationLockTableName: "migration_locks",
|
|
63
63
|
});
|
|
64
|
-
const { error } = await migrator.migrateToLatest();
|
|
64
|
+
const { error, results } = await migrator.migrateToLatest();
|
|
65
|
+
// A schema change is the least reversible thing an app does at boot, and the
|
|
66
|
+
// per-migration outcome was being discarded — so a run that applied four
|
|
67
|
+
// migrations and a run that found none to apply looked identical afterwards.
|
|
68
|
+
// `info`, because which migrations a deployment applied is the fact you go
|
|
69
|
+
// looking for when a schema is not what you expected.
|
|
70
|
+
// `sql.migration.name`, not `db.migration.name`: OTel owns `db.*` and defines
|
|
71
|
+
// no migration attribute, and §6.2 forbids inventing keys inside a namespace
|
|
72
|
+
// someone else governs.
|
|
73
|
+
for (const applied of results ?? []) {
|
|
74
|
+
if (applied.status === "Success") {
|
|
75
|
+
this.ctx.log.info("Migration applied", { "sql.migration.name": applied.migrationName });
|
|
76
|
+
}
|
|
77
|
+
else if (applied.status === "Error") {
|
|
78
|
+
// The cause rides on the record: this is the error-severity line an
|
|
79
|
+
// operator finds first, and the migration name alone cannot say what
|
|
80
|
+
// went wrong. `error` keeps the type, stack and cause chain (§4.2).
|
|
81
|
+
this.ctx.log.error("Migration failed", { "sql.migration.name": applied.migrationName }, { error });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
65
84
|
if (error) {
|
|
66
85
|
throw error;
|
|
67
86
|
}
|
|
87
|
+
if (!results?.length) {
|
|
88
|
+
this.ctx.log.debug("No pending migrations");
|
|
89
|
+
}
|
|
68
90
|
}
|
|
69
91
|
}
|
|
70
92
|
function failMissingConnection() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"@types/node": "^20.0.0",
|
|
44
44
|
"esbuild": "^0.25.12",
|
|
45
45
|
"typescript": "^5.0.0",
|
|
46
|
-
"@telorun/sdk": "0.
|
|
46
|
+
"@telorun/sdk": "0.75.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@telorun/sdk": "*"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"build": "tsc -p tsconfig.lib.json
|
|
52
|
+
"build": "tsc -p tsconfig.lib.json"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
InvokeError,
|
|
3
|
+
SEVERITY,
|
|
3
4
|
type InvokeContext,
|
|
4
5
|
type ResourceContext,
|
|
5
6
|
type ZoneEntry,
|
|
@@ -50,15 +51,38 @@ export abstract class SqlConnectionBase implements SqlConnection {
|
|
|
50
51
|
async runInTransaction<T>(
|
|
51
52
|
body: (bind: (entry: ZoneEntry) => void) => Promise<T>,
|
|
52
53
|
): Promise<T> {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
this.ctx.log.debug("Transaction started");
|
|
55
|
+
try {
|
|
56
|
+
const result = await this.db
|
|
57
|
+
.transaction()
|
|
58
|
+
.execute((trx: Transaction<any>) => body((entry) => this.#executors.set(entry, trx)));
|
|
59
|
+
this.ctx.log.debug("Transaction committed");
|
|
60
|
+
return result;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// The error reaches the caller, but the ROLLBACK does not: a caller that
|
|
63
|
+
// maps the failure to a response sees nothing saying its writes were
|
|
64
|
+
// discarded, and that is the fact worth reconstructing afterwards.
|
|
65
|
+
this.ctx.log.debug("Transaction rolled back", undefined, { error: err });
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
hasOpenTransaction(ctx?: InvokeContext): boolean {
|
|
59
71
|
return this.ctx.zonesFor(this, ctx).some((entry) => this.#executors.has(entry));
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Every statement this connection runs funnels through here — `executeTemplate`
|
|
76
|
+
* and `executeScript` both delegate — so it is the single instrumentation point.
|
|
77
|
+
*
|
|
78
|
+
* `db.query.text` is the statement, never the parameters: the values ARE the
|
|
79
|
+
* data, and a record carrying them would put row contents in the log. The
|
|
80
|
+
* statement itself is safe for a parameterized query (it is the template), and
|
|
81
|
+
* is `debug`-only regardless, because `Sql.Command` can carry inline literals.
|
|
82
|
+
*
|
|
83
|
+
* The disabled path allocates nothing and takes no clock reading — a query is
|
|
84
|
+
* the hottest thing this module does.
|
|
85
|
+
*/
|
|
62
86
|
async execute<T>(
|
|
63
87
|
sql: string,
|
|
64
88
|
params: unknown[] = [],
|
|
@@ -66,7 +90,22 @@ export abstract class SqlConnectionBase implements SqlConnection {
|
|
|
66
90
|
ctx?: InvokeContext,
|
|
67
91
|
): Promise<QueryResult<T>> {
|
|
68
92
|
const executor = this.resolveExecutor(zone, ctx);
|
|
69
|
-
|
|
93
|
+
if (!this.ctx.log.enabled(SEVERITY.debug)) {
|
|
94
|
+
return executor.executeQuery<T>(CompiledQuery.raw(sql, params));
|
|
95
|
+
}
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
const result = await executor.executeQuery<T>(CompiledQuery.raw(sql, params));
|
|
98
|
+
this.ctx.log.debug("Statement executed", {
|
|
99
|
+
"db.query.text": sql,
|
|
100
|
+
"db.response.returned_rows": result.rows.length,
|
|
101
|
+
// OTel's own name for this quantity, in OTel's own unit: SECONDS, as a
|
|
102
|
+
// double. Metric names and attribute keys are separate namespaces, so
|
|
103
|
+
// reusing the name is safe — what would not be safe is the name with the
|
|
104
|
+
// wrong magnitude, which is why this is not milliseconds. Units live in
|
|
105
|
+
// the convention, never in the key.
|
|
106
|
+
"db.client.operation.duration": (Date.now() - startedAt) / 1000,
|
|
107
|
+
});
|
|
108
|
+
return result;
|
|
70
109
|
}
|
|
71
110
|
|
|
72
111
|
async executeTemplate<T>(
|
|
@@ -97,10 +97,35 @@ class SqlMigrationsResource implements ResourceInstance {
|
|
|
97
97
|
migrationLockTableName: "migration_locks",
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
const { error } = await migrator.migrateToLatest();
|
|
100
|
+
const { error, results } = await migrator.migrateToLatest();
|
|
101
|
+
// A schema change is the least reversible thing an app does at boot, and the
|
|
102
|
+
// per-migration outcome was being discarded — so a run that applied four
|
|
103
|
+
// migrations and a run that found none to apply looked identical afterwards.
|
|
104
|
+
// `info`, because which migrations a deployment applied is the fact you go
|
|
105
|
+
// looking for when a schema is not what you expected.
|
|
106
|
+
// `sql.migration.name`, not `db.migration.name`: OTel owns `db.*` and defines
|
|
107
|
+
// no migration attribute, and §6.2 forbids inventing keys inside a namespace
|
|
108
|
+
// someone else governs.
|
|
109
|
+
for (const applied of results ?? []) {
|
|
110
|
+
if (applied.status === "Success") {
|
|
111
|
+
this.ctx.log.info("Migration applied", { "sql.migration.name": applied.migrationName });
|
|
112
|
+
} else if (applied.status === "Error") {
|
|
113
|
+
// The cause rides on the record: this is the error-severity line an
|
|
114
|
+
// operator finds first, and the migration name alone cannot say what
|
|
115
|
+
// went wrong. `error` keeps the type, stack and cause chain (§4.2).
|
|
116
|
+
this.ctx.log.error(
|
|
117
|
+
"Migration failed",
|
|
118
|
+
{ "sql.migration.name": applied.migrationName },
|
|
119
|
+
{ error },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
101
123
|
if (error) {
|
|
102
124
|
throw error;
|
|
103
125
|
}
|
|
126
|
+
if (!results?.length) {
|
|
127
|
+
this.ctx.log.debug("No pending migrations");
|
|
128
|
+
}
|
|
104
129
|
}
|
|
105
130
|
}
|
|
106
131
|
|