@crvouga/mockingbird-service-postgres 3.1.0 → 3.3.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/COMPATIBILITY.md +35 -5
  3. package/README.md +147 -4
  4. package/compat/coverage.json +8 -7
  5. package/compat/divergences.json +37 -2
  6. package/compat/requirements.json +1 -1
  7. package/compat/sections/cat.ts +49 -0
  8. package/compat/sections/ddl.ts +11 -0
  9. package/compat/unsupported-register.json +0 -3
  10. package/dist/admin.js +3330 -668
  11. package/dist/admin.js.map +4 -4
  12. package/dist/api/database.d.ts +11 -0
  13. package/dist/ast/nodes.d.ts +43 -1
  14. package/dist/constraints/enforce.d.ts +2 -0
  15. package/dist/executor/alter-column.d.ts +27 -0
  16. package/dist/executor/comment.d.ts +4 -0
  17. package/dist/executor/ddl.d.ts +1 -2
  18. package/dist/executor/dml.d.ts +2 -0
  19. package/dist/executor/extensions.d.ts +11 -0
  20. package/dist/executor/maintenance.d.ts +25 -0
  21. package/dist/expressions/context.d.ts +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +3249 -549
  24. package/dist/index.js.map +4 -4
  25. package/dist/parser/parser.d.ts +9 -0
  26. package/dist/schema/catalog-extensions.d.ts +5 -0
  27. package/dist/schema/extensions.d.ts +33 -0
  28. package/dist/schema/index-catalog.d.ts +40 -0
  29. package/dist/schema/persistence.d.ts +32 -0
  30. package/dist/schema/storage.d.ts +22 -0
  31. package/dist/server.js +5384 -743
  32. package/dist/server.js.map +4 -4
  33. package/dist/sql/deparse-default.d.ts +12 -0
  34. package/dist/sql/deparse-index.d.ts +12 -2
  35. package/dist/sql/keywords.d.ts +6 -0
  36. package/dist/sql/ruleutils.d.ts +27 -0
  37. package/dist/storage/database-state.d.ts +51 -0
  38. package/dist/types/regnamespace.d.ts +23 -0
  39. package/dist/types/value.d.ts +2 -0
  40. package/dist/unstable.js +11520 -8875
  41. package/dist/unstable.js.map +4 -4
  42. package/dist/wire/cli.js +5396 -750
  43. package/dist/wire/cli.js.map +4 -4
  44. package/dist/wire/cluster.d.ts +8 -0
  45. package/dist/wire/connection.d.ts +38 -0
  46. package/dist/wire/durable.d.ts +82 -0
  47. package/dist/wire/file-storage.d.ts +3 -0
  48. package/dist/wire/fleet.d.ts +2 -0
  49. package/dist/wire/index.d.ts +21 -1
  50. package/dist/wire/index.js +5383 -741
  51. package/dist/wire/index.js.map +4 -4
  52. package/dist/wire/startup.d.ts +36 -0
  53. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { Database } from "../api/database.js";
2
2
  import type { Snapshot } from "../api/snapshot.js";
3
+ import type { Durability } from "./durable.js";
3
4
  /**
4
5
  * What every connection of one server shares: the engine, and the coordination the engine
5
6
  * (single-session, synchronous) does not do itself.
@@ -28,6 +29,8 @@ export declare class Cluster {
28
29
  current: Session | null;
29
30
  private nextPid;
30
31
  private readonly databases;
32
+ /** Set in durable mode: commits are persisted through it before they are acknowledged. */
33
+ durability: Durability | null;
31
34
  constructor(db: Database, defaultDatabaseName?: string);
32
35
  /** The initial database, retained for the server's backwards-compatible public handle. */
33
36
  get db(): Database;
@@ -35,6 +38,11 @@ export declare class Cluster {
35
38
  snapshotAll(): Map<string, Snapshot>;
36
39
  restoreAll(points: ReadonlyMap<string, Snapshot>): void;
37
40
  getDatabase(name: string): Database | undefined;
41
+ /** Add a database that already exists (restored from durable storage) to the catalog. */
42
+ attach(name: string, db: Database): void;
43
+ /** The catalog as it stands, to put back with {@link restoreCatalog} if a change cannot be persisted. */
44
+ catalog(): Map<string, Database>;
45
+ restoreCatalog(entries: ReadonlyMap<string, Database>): void;
38
46
  requireDatabase(name: string): Database;
39
47
  createDatabase(name: string, template?: string): void;
40
48
  dropDatabase(name: string, requester: Session): void;
@@ -8,6 +8,11 @@ export type ServerFaults = {
8
8
  delayStatementMs?: number;
9
9
  /** Fail the next COMMIT with this SQLSTATE (`40001` serialization failure) after rolling back. */
10
10
  failCommit?: string;
11
+ /**
12
+ * Interrupt the next `CREATE INDEX CONCURRENTLY` / `REINDEX ... CONCURRENTLY` with this SQLSTATE
13
+ * (`57014` canceled, `40P01` deadlock). The index it was building stays in the catalog, invalid.
14
+ */
15
+ failConcurrentIndexBuild?: string;
11
16
  };
12
17
  export type ServerLog = {
13
18
  pid: number;
@@ -67,11 +72,24 @@ export declare class Connection implements Session {
67
72
  /** Successful writes in this session's READ COMMITTED transaction workspace. */
68
73
  private transaction;
69
74
  private closed;
75
+ /** This session's settings. They stand in for the database's own around each of its engine calls. */
76
+ private settings;
77
+ /** What `RESET` returns to: the database's settings at connect, then the startup packet's. */
78
+ private settingDefaults;
79
+ /** Durable commits that are persisted and not yet acknowledged on the socket. */
80
+ private readonly unacknowledged;
81
+ /** A commit of this session is being persisted; it still owns the statement turn. */
82
+ private persisting;
70
83
  constructor(socket: Socket, cluster: Cluster, options: ConnectionOptions);
71
84
  private receive;
72
85
  private pump;
73
86
  private write;
74
87
  private dispose;
88
+ /**
89
+ * Run engine work with this session's settings in place of the database's own, so one
90
+ * connection's `SET` never reaches another. Whatever the work leaves behind is the session's.
91
+ */
92
+ private inSession;
75
93
  /** Terminate from the server side (close, or the drop-connection fault). */
76
94
  destroy(): void;
77
95
  queueNotification(pid: number, channel: string, payload: string): void;
@@ -92,6 +110,24 @@ export declare class Connection implements Session {
92
110
  /** Run one statement against the engine, honoring the turn, locks, aborted state and faults. */
93
111
  private execute;
94
112
  private executeInner;
113
+ /**
114
+ * Durable mode, before a statement that may commit: how to take that commit back out of
115
+ * memory if storage then refuses it. Null when the statement cannot commit a change.
116
+ */
117
+ private durableUndo;
118
+ /** What a transaction's BEGIN captures and its ROLLBACK restores, for a commit already applied. */
119
+ private checkpoint;
120
+ /**
121
+ * Persist a commit this session just applied, while it still holds the statement turn, so no
122
+ * other session has seen it. Storage refusing it takes the commit back out of memory and
123
+ * fails the statement (`58030`, or `53100` when storage is full); the transaction is gone,
124
+ * as after any failed COMMIT. Resolves to the commit when storage was written.
125
+ */
126
+ private persist;
127
+ /** The acknowledgement of every persisted commit is on the socket. */
128
+ private acknowledged;
129
+ /** CREATE / DROP / ALTER DATABASE in durable mode: the catalog change is persisted, or undone. */
130
+ private durableClusterDdl;
95
131
  private waitForLock;
96
132
  /** The engine call itself: statement-atomic, and transaction control handled as the server sees it. */
97
133
  private runStatement;
@@ -119,6 +155,8 @@ export declare class Connection implements Session {
119
155
  private copyStatement;
120
156
  private copyColumnCount;
121
157
  private handleCopyIn;
158
+ /** Engine work of a COPY in progress. A workspace already carries the session's settings. */
159
+ private inCopy;
122
160
  private beginCopyIn;
123
161
  private finishCopyIn;
124
162
  private flushCopyRecords;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Opt-in durable mode for the wire server: a commit is acknowledged only once the whole cluster,
3
+ * as PGMM snapshots, has reached a {@link DurableStorage}. It is snapshot persistence, not a
4
+ * write-ahead log and not PostgreSQL's on-disk format. The guarantee it models is the one a
5
+ * client relies on: https://www.postgresql.org/docs/18/wal-intro.html (an acknowledged COMMIT
6
+ * survives a crash) and https://www.postgresql.org/docs/18/sql-commit.html.
7
+ *
8
+ * Nothing here touches a filesystem: the storage is a port the server awaits, so a test can
9
+ * model failure and pauses, and {@link fileStorage} (./file-storage.ts) is the real one.
10
+ */
11
+ import "../serialization/codec.js";
12
+ import { type Database } from "../api/database.js";
13
+ import { PostgresError } from "../errors/error.js";
14
+ import type { Cluster } from "./cluster.js";
15
+ /** One durable image of the cluster: each database's PGMM snapshot bytes by database name. */
16
+ export type ClusterImage = ReadonlyMap<string, Uint8Array>;
17
+ /** Where durable mode keeps the cluster. The server awaits it before acknowledging a commit. */
18
+ export interface DurableStorage {
19
+ /** The image of the last {@link write} that resolved, or `null` when nothing was persisted yet. */
20
+ read(): Promise<ClusterImage | null>;
21
+ /**
22
+ * Replace the persisted image with `image`, atomically: resolve once it would survive the
23
+ * process being killed, or reject and leave the previous image intact. The server never
24
+ * calls it concurrently, and passes the same `Uint8Array` object for a database that did
25
+ * not change since the last call, so an implementation can skip rewriting it.
26
+ */
27
+ write(image: ClusterImage): Promise<void>;
28
+ }
29
+ /** A commit the server persisted, as the test controls of {@link DurableOptions} see it. */
30
+ export type DurableCommit = {
31
+ /** Backend pid of the session that committed. */
32
+ pid: number;
33
+ /** Database the commit belongs to. */
34
+ database: string;
35
+ /** The statement that committed: the write itself in autocommit, else `COMMIT`. */
36
+ sql: string;
37
+ };
38
+ export type DurableOptions = {
39
+ /** A directory for the file-backed store, or an injected {@link DurableStorage}. */
40
+ storage: string | DurableStorage;
41
+ /**
42
+ * Test control: awaited after a commit was persisted and before the server acknowledges it.
43
+ * A process killed while this is pending restarts with the commit; the client never saw it.
44
+ */
45
+ beforeAcknowledge?: (commit: DurableCommit) => void | Promise<void>;
46
+ /**
47
+ * Test control: awaited right after the acknowledgement was written to the socket
48
+ * (`CommandComplete`, or `ReadyForQuery` for the implicit commit of a multi-statement query).
49
+ */
50
+ afterAcknowledge?: (commit: DurableCommit) => void | Promise<void>;
51
+ };
52
+ /**
53
+ * A storage failure as the client sees it: the commit is not acknowledged, and the statement
54
+ * ends with `58030` (io_error), or `53100` (disk_full) when the storage ran out of space.
55
+ */
56
+ export declare const storageFailure: (cause: unknown) => PostgresError;
57
+ /** Rebuild the databases of a persisted image, with the runtime (clock, seed, entropy) of `template`. */
58
+ export declare const openImage: (image: ClusterImage, template: Database) => Map<string, Database>;
59
+ /** Persists the cluster for the server, re-encoding only the databases a commit touched. */
60
+ export declare class Durability {
61
+ private readonly cluster;
62
+ private readonly storage;
63
+ private readonly hooks;
64
+ /** PGMM bytes of each database as storage holds them. */
65
+ private readonly persisted;
66
+ private image;
67
+ private tail;
68
+ constructor(cluster: Cluster, storage: DurableStorage, hooks: Pick<DurableOptions, "beforeAcknowledge" | "afterAcknowledge">);
69
+ /** Record that storage already holds `image` for the cluster's databases (after a restart). */
70
+ adopt(image: ClusterImage): void;
71
+ /**
72
+ * Write the cluster with `changed` (or every database) encoded from its live state. Resolves
73
+ * to whether storage was written: an image identical to the persisted one is skipped. The
74
+ * caller holds the statement turn, so no session runs between the commit and this.
75
+ */
76
+ persist(changed: readonly Database[] | "all"): Promise<boolean>;
77
+ /** Resolves when no write is in flight. */
78
+ idle(): Promise<void>;
79
+ private write;
80
+ beforeAcknowledge(commit: DurableCommit): void | Promise<void>;
81
+ afterAcknowledge(commit: DurableCommit): void | Promise<void>;
82
+ }
@@ -0,0 +1,3 @@
1
+ import type { DurableStorage } from "./durable.js";
2
+ /** Persist a cluster under the directory `root` (created when missing). One server per root. */
3
+ export declare const fileStorage: (root: string) => DurableStorage;
@@ -66,6 +66,8 @@ type ConfigService = {
66
66
  adminPrefix?: string;
67
67
  adminKey?: string;
68
68
  seed?: string;
69
+ fixtures?: Record<string, unknown>;
70
+ baseUrl?: string;
69
71
  password?: string;
70
72
  user?: string;
71
73
  database?: string;
@@ -11,6 +11,9 @@
11
11
  * transaction blocks. Advisory locks, `LISTEN`/`NOTIFY`, `CancelRequest` and per-connection
12
12
  * aborted-transaction state (`25P02`) are coordinated across connections.
13
13
  *
14
+ * State lives in memory. The opt-in `durable` option persists the cluster before each commit is
15
+ * acknowledged (see ./durable.ts); without it the server touches no filesystem.
16
+ *
14
17
  * @example
15
18
  * ```ts
16
19
  * import { serve } from "@crvouga/mockingbird-service-postgres/wire";
@@ -26,6 +29,7 @@ import { type Server } from "node:net";
26
29
  import { Database } from "../api/database.js";
27
30
  import type { Snapshot } from "../api/snapshot.js";
28
31
  import { type ServerFaults, type ServerLog } from "./connection.js";
32
+ import { type DurableOptions, type DurableStorage } from "./durable.js";
29
33
  export type ServeOptions = {
30
34
  /** TCP port; `0` (the default) picks a free one, reported as `server.port`. */
31
35
  port?: number;
@@ -59,6 +63,15 @@ export type ServeOptions = {
59
63
  allowDatabaseSelection?: boolean;
60
64
  /** Role shown in `connectionString`. A URI's username fills this. Default `postgres`. */
61
65
  user?: string;
66
+ /**
67
+ * Opt-in durable mode: a directory (the file-backed store), an injected {@link DurableStorage},
68
+ * or {@link DurableOptions} with test controls. An autocommit write or `COMMIT` is then
69
+ * persisted before it is acknowledged, and a server started on the same storage resumes from
70
+ * the last acknowledged commit, even after a `SIGKILL`. Storage that already holds a cluster
71
+ * wins over `database`, which then only supplies the clock, seed and entropy mode. Omitted
72
+ * (the default), state lives in memory only and no filesystem is touched.
73
+ */
74
+ durable?: string | DurableStorage | DurableOptions;
62
75
  };
63
76
  export type PostgresServer = {
64
77
  /** The bound port. */
@@ -85,7 +98,12 @@ export type PostgresServer = {
85
98
  snapshotAll(): Map<string, Snapshot>;
86
99
  restoreAll(points: ReadonlyMap<string, Snapshot>): void;
87
100
  faultState(): ServerFaults;
88
- /** Stop listening and close every connection. */
101
+ /**
102
+ * Durable mode: persist every database now, for changes made through `database` /
103
+ * `getDatabase()` / `restoreAll()` rather than a connection. Resolves at once without `durable`.
104
+ */
105
+ persist(): Promise<void>;
106
+ /** Stop listening and close every connection. Durable mode also waits for a write in flight. */
89
107
  close(): Promise<void>;
90
108
  };
91
109
  export type ServeInput = ServeOptions | string;
@@ -93,3 +111,5 @@ export type ServeInput = ServeOptions | string;
93
111
  export declare const serve: (input?: ServeInput) => Promise<PostgresServer>;
94
112
  export { Cluster } from "./cluster.js";
95
113
  export type { ServerFaults, ServerLog } from "./connection.js";
114
+ export type { ClusterImage, DurableCommit, DurableOptions, DurableStorage } from "./durable.js";
115
+ export { fileStorage } from "./file-storage.js";