@tablecraft-app/plugin-postgres 1.12.1 → 1.14.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/index.js CHANGED
@@ -10823,7 +10823,8 @@ var CAPABILITIES$1 = [
10823
10823
  "commands",
10824
10824
  "keyValue",
10825
10825
  "nodes",
10826
- "serverInfo"
10826
+ "serverInfo",
10827
+ "serverTimeout"
10827
10828
  ];
10828
10829
  /** The message that stands for the ones past `SERVER_MESSAGE_LIMIT`. */
10829
10830
  function omittedMessages(count) {
@@ -11025,6 +11026,11 @@ function urlSetsDeclared(connection) {
11025
11026
  if (url?.sets === void 0) return true;
11026
11027
  return Object.entries(url.sets).every(([scheme, values]) => url.schemes.includes(scheme) && Object.keys(values).every((key) => connection.fields.some((field) => field.key === key)));
11027
11028
  }
11029
+ /** A database product a plugin speaks; `id` is a simple-icons slug, `name` what a row shows. */
11030
+ var brandSchema = strictObject({
11031
+ id: string$1().regex(/^[a-z0-9_]+$/, { message: "expected a simple-icons slug: lower-case letters, digits and underscores" }),
11032
+ name: string$1().min(1)
11033
+ });
11028
11034
  function includesAny(capabilities, ...wanted) {
11029
11035
  return wanted.some((capability) => capabilities.includes(capability));
11030
11036
  }
@@ -11038,6 +11044,8 @@ strictObject({
11038
11044
  capabilities: array(_enum(CAPABILITIES$1)),
11039
11045
  /** The highlighter the console uses for this plugin's dialect (decision 008). */
11040
11046
  highlight: string$1().min(1),
11047
+ /** The database products the plugin speaks, its default first (decision 128). Absent: rows name the plugin. */
11048
+ brands: array(brandSchema).min(1).refine((brands) => new Set(brands.map((brand) => brand.id)).size === brands.length, { message: "brand ids must be unique" }).optional(),
11041
11049
  /** The connection form, readable without running plugin code (decision 019). */
11042
11050
  connection: strictObject({
11043
11051
  fields: array(connectionFieldSchema).min(1),
@@ -25225,6 +25233,39 @@ function postgresSessions(client, open) {
25225
25233
  terminateSession: (target) => stop(target, terminateStatement, "terminate")
25226
25234
  };
25227
25235
  }
25236
+ // ../../plugins/postgres/src/timeout.ts
25237
+ /**
25238
+ * PostgreSQL's `statement_timeout` covers every statement and, measured against PostgreSQL 17,
25239
+ * keeps counting while a `pg-cursor` result waits to be read: the cursor's `Execute` is sent
25240
+ * without a `Sync`, so the statement is still running (decision 129).
25241
+ */
25242
+ var POSTGRES_SERVER_TIMEOUT = {
25243
+ setting: "statement_timeout",
25244
+ covers: "all",
25245
+ counts: "statement"
25246
+ };
25247
+ var READ_TIMEOUT_SQL = "SHOW statement_timeout";
25248
+ var SET_TIMEOUT_SQL = "SELECT set_config('statement_timeout', $1, false)";
25249
+ /** A statement naming the setting may have changed it behind the plugin's back. */
25250
+ var TIMEOUT_SETTING = /statement_timeout/i;
25251
+ function namesServerTimeout(sql) {
25252
+ return TIMEOUT_SETTING.test(sql);
25253
+ }
25254
+ /** `SHOW`'s units for a time setting, in milliseconds; a bare number is milliseconds already. */
25255
+ var UNIT_MS = {
25256
+ ms: 1,
25257
+ s: 1e3,
25258
+ min: 6e4,
25259
+ h: 36e5,
25260
+ d: 864e5
25261
+ };
25262
+ var DURATION = /^(\d+(?:\.\d+)?)\s*(ms|s|min|h|d)?$/;
25263
+ /** `SHOW statement_timeout` as milliseconds: `5s`, `1min`, `250ms`, `0`; anything unreadable is 0, no limit. */
25264
+ function postgresDurationMs(text) {
25265
+ const match = DURATION.exec(text.trim());
25266
+ if (match === null) return 0;
25267
+ return Math.round(Number(match[1]) * (UNIT_MS[match[2] ?? "ms"] ?? 1));
25268
+ }
25228
25269
  // ../../plugins/postgres/src/tls.ts
25229
25270
  var SSL_MODES = [
25230
25271
  "disable",
@@ -25323,6 +25364,10 @@ function tlsInfoOf(socket) {
25323
25364
  }
25324
25365
  // ../../plugins/postgres/src/index.ts
25325
25366
  var PLUGIN_ID = "postgres";
25367
+ var CANCEL_REACH = {
25368
+ method: "pg_cancel_backend",
25369
+ reaches: "statement"
25370
+ };
25326
25371
  var CAPABILITIES = [
25327
25372
  "sql",
25328
25373
  "cancel",
@@ -25337,7 +25382,8 @@ var CAPABILITIES = [
25337
25382
  "objects",
25338
25383
  "serverMessages",
25339
25384
  "sessions",
25340
- "routineCall"
25385
+ "routineCall",
25386
+ "serverTimeout"
25341
25387
  ];
25342
25388
  /** Function bodies live in dollar quotes; `BEGIN ATOMIC … END` holds a SQL-standard body. */
25343
25389
  var DIALECT = {
@@ -25433,6 +25479,7 @@ var RELEASE_SQL = "RELEASE SAVEPOINT table_craft_insert";
25433
25479
  var ROLLBACK_TO_SQL = "ROLLBACK TO SAVEPOINT table_craft_insert";
25434
25480
  var OPEN_TRANSACTION_MESSAGE = "A transaction is open; commit or roll it back before switching to auto-commit.";
25435
25481
  var BUSY_MESSAGE = "Another statement is still running in this session.";
25482
+ var CANCELLED_MESSAGE = "The statement was cancelled.";
25436
25483
  var DEFAULT_PORT = 5432;
25437
25484
  var MAX_PORT = 65535;
25438
25485
  var DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;
@@ -25560,6 +25607,18 @@ async function openClient(config, options) {
25560
25607
  }
25561
25608
  return client;
25562
25609
  }
25610
+ function startRunning() {
25611
+ return {
25612
+ cancelRequested: false,
25613
+ startedAt: Date.now(),
25614
+ limitMs: null
25615
+ };
25616
+ }
25617
+ /**
25618
+ * `#appliedTimeoutMs` once a transaction ended after the plugin set the limit: a `ROLLBACK` may
25619
+ * have undone it, so the next statement sets it again whatever it asks for.
25620
+ */
25621
+ var UNKNOWN_TIMEOUT = -1;
25563
25622
  function readBatch(cursor) {
25564
25623
  return new Promise((resolve, reject) => {
25565
25624
  cursor.read(FETCH_ROWS, (error, rows, result) => {
@@ -25607,7 +25666,15 @@ var PostgresSession = class PostgresSession {
25607
25666
  mode: "auto",
25608
25667
  open: false
25609
25668
  };
25669
+ /** The session's own `statement_timeout` in ms, 0 for none; read before the first limited statement. */
25670
+ #ownTimeoutMs = null;
25671
+ /** The limit this plugin last set; null before it set one; `UNKNOWN_TIMEOUT` after a transaction ended. */
25672
+ #appliedTimeoutMs = null;
25610
25673
  serverVersion;
25674
+ /** The manifest's one brand. */
25675
+ serverBrand = "postgresql";
25676
+ serverTimeout = POSTGRES_SERVER_TIMEOUT;
25677
+ cancelReach = CANCEL_REACH;
25611
25678
  tls;
25612
25679
  introspector;
25613
25680
  transactions;
@@ -25661,7 +25728,7 @@ var PostgresSession = class PostgresSession {
25661
25728
  if (this.#broken !== null) throw new PluginError("connectionLost", `The connection was lost: ${this.#broken.message}`);
25662
25729
  if (this.#running !== null) throw new PluginError("sessionBusy", BUSY_MESSAGE);
25663
25730
  await this.#cancelling.catch(() => void 0);
25664
- const running = { cancelRequested: false };
25731
+ const running = startRunning();
25665
25732
  this.#running = running;
25666
25733
  const effect = transactionEffect(sql, DIALECT);
25667
25734
  const state = this.#transaction;
@@ -25670,6 +25737,10 @@ var PostgresSession = class PostgresSession {
25670
25737
  await this.#client.query(BEGIN_SQL);
25671
25738
  state.open = true;
25672
25739
  }
25740
+ running.limitMs = await this.#applyTimeout(options.serverTimeoutMs);
25741
+ if (namesServerTimeout(sql)) this.#appliedTimeoutMs = null;
25742
+ if (running.cancelRequested) throw new Error(CANCELLED_MESSAGE);
25743
+ running.startedAt = Date.now();
25673
25744
  } catch (cause) {
25674
25745
  this.#running = null;
25675
25746
  throw this.#failure(cause, running);
@@ -25682,10 +25753,11 @@ var PostgresSession = class PostgresSession {
25682
25753
  }));
25683
25754
  try {
25684
25755
  const first = await readBatch(cursor).catch((cause) => {
25685
- if (effect === "end") state.open = false;
25756
+ if (effect === "end") this.#transactionEnded();
25686
25757
  throw cause;
25687
25758
  });
25688
- if (effect !== null) state.open = effect === "begin";
25759
+ if (effect === "end") this.#transactionEnded();
25760
+ else if (effect !== null) state.open = true;
25689
25761
  const fields = first.result.fields;
25690
25762
  if (fields.length === 0) {
25691
25763
  this.#notices = null;
@@ -25740,7 +25812,7 @@ var PostgresSession = class PostgresSession {
25740
25812
  if (this.#running !== null) throw new PluginError("sessionBusy", BUSY_MESSAGE);
25741
25813
  if (rows.length === 0) return;
25742
25814
  await this.#cancelling.catch(() => void 0);
25743
- const running = { cancelRequested: false };
25815
+ const running = startRunning();
25744
25816
  this.#running = running;
25745
25817
  const state = this.#transaction;
25746
25818
  try {
@@ -25783,7 +25855,8 @@ var PostgresSession = class PostgresSession {
25783
25855
  if (!verdict.ok) throw new PluginError("explainUnsupported", verdict.reason);
25784
25856
  const statement = explainStatement(sql, options.analyze);
25785
25857
  const rows = [];
25786
- const result = await this.execute(statement);
25858
+ const limit = options.serverTimeoutMs;
25859
+ const result = await this.execute(statement, [], limit === void 0 ? {} : { serverTimeoutMs: limit });
25787
25860
  for await (const row of result.rows) rows.push(row);
25788
25861
  return {
25789
25862
  ...readPlan(planText(rows), statement, options.analyze),
@@ -25815,7 +25888,9 @@ var PostgresSession = class PostgresSession {
25815
25888
  }
25816
25889
  #failure(cause, running, sql) {
25817
25890
  const code = cause.code;
25818
- if (running.cancelRequested || code === QUERY_CANCELED) return new PluginError("cancelled", "The statement was cancelled.", { cause });
25891
+ const limit = running.limitMs;
25892
+ if (!running.cancelRequested && code === QUERY_CANCELED && limit !== null && Date.now() - running.startedAt >= limit) return new PluginError("timedOut", `PostgreSQL stopped the statement at its ${String(limit / MS_PER_SECOND)} s limit (statement_timeout).`, { cause });
25893
+ if (running.cancelRequested || code === QUERY_CANCELED) return new PluginError("cancelled", CANCELLED_MESSAGE, { cause });
25819
25894
  const position = sql === void 0 ? void 0 : serverErrorPosition(sql, cause);
25820
25895
  return new PluginError("statementFailed", messageOf(cause), {
25821
25896
  cause,
@@ -25852,7 +25927,37 @@ var PostgresSession = class PostgresSession {
25852
25927
  } catch (cause) {
25853
25928
  throw new PluginError("statementFailed", messageOf(cause), { cause });
25854
25929
  } finally {
25855
- this.#transaction.open = false;
25930
+ this.#transactionEnded();
25931
+ }
25932
+ }
25933
+ /** `statement_timeout` is transactional: a `ROLLBACK` undoes a `SET` made inside it. */
25934
+ #transactionEnded() {
25935
+ this.#transaction.open = false;
25936
+ if (this.#appliedTimeoutMs !== null) this.#appliedTimeoutMs = UNKNOWN_TIMEOUT;
25937
+ }
25938
+ /**
25939
+ * Sets `statement_timeout` to `requested`, never above the session's own, or puts the own back
25940
+ * when nothing is requested; sends nothing when the value would not change (decision 129). In a
25941
+ * failed transaction, which refuses the `SET`, it is skipped and the statement fails on its own.
25942
+ * Returns the limit the statement runs under, null for none.
25943
+ */
25944
+ async #applyTimeout(requested) {
25945
+ if (requested === void 0 && this.#appliedTimeoutMs === null) return null;
25946
+ try {
25947
+ if (this.#ownTimeoutMs === null) {
25948
+ const shown = await this.#client.query(READ_TIMEOUT_SQL);
25949
+ this.#ownTimeoutMs = postgresDurationMs(shown.rows[0]?.statement_timeout ?? "0");
25950
+ }
25951
+ const own = this.#ownTimeoutMs;
25952
+ const want = requested === void 0 ? own : own === 0 ? requested : Math.min(own, requested);
25953
+ if (want !== this.#appliedTimeoutMs) {
25954
+ await this.#client.query(SET_TIMEOUT_SQL, [String(want)]);
25955
+ this.#appliedTimeoutMs = want;
25956
+ }
25957
+ return want === 0 ? null : want;
25958
+ } catch (cause) {
25959
+ if (cause.code !== "25P02") throw cause;
25960
+ return null;
25856
25961
  }
25857
25962
  }
25858
25963
  async close() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablecraft-app/plugin-postgres",
3
- "version": "1.12.1",
3
+ "version": "1.14.0",
4
4
  "description": "The PostgreSQL datasource plugin for TableCraft, downloaded by the app when a data source needs it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,6 +24,6 @@
24
24
  },
25
25
  "tableCraft": {
26
26
  "id": "postgres",
27
- "apiVersion": "1.5.0"
27
+ "apiVersion": "1.7.0"
28
28
  }
29
29
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "postgres",
3
3
  "name": "PostgreSQL",
4
- "version": "1.12.1",
5
- "apiVersion": "1.5.0",
4
+ "version": "1.14.0",
5
+ "apiVersion": "1.7.0",
6
6
  "entry": "index.js",
7
7
  "capabilities": [
8
8
  "sql",
@@ -18,9 +18,16 @@
18
18
  "objects",
19
19
  "serverMessages",
20
20
  "sessions",
21
- "routineCall"
21
+ "routineCall",
22
+ "serverTimeout"
22
23
  ],
23
24
  "highlight": "postgresql",
25
+ "brands": [
26
+ {
27
+ "id": "postgresql",
28
+ "name": "PostgreSQL"
29
+ }
30
+ ],
24
31
  "connection": {
25
32
  "fields": [
26
33
  {