@effectstream/db 0.100.12 → 0.100.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/db",
3
- "version": "0.100.12",
3
+ "version": "0.100.14",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -21,11 +21,11 @@
21
21
  "./version": "./migrations/system-version.ts"
22
22
  },
23
23
  "dependencies": {
24
- "@effectstream/coroutine": "0.100.12",
25
- "@effectstream/log": "0.100.12",
26
- "@effectstream/runtime": "0.100.12",
27
- "@effectstream/sync": "0.100.12",
28
- "@effectstream/utils": "0.100.12",
24
+ "@effectstream/coroutine": "0.100.14",
25
+ "@effectstream/log": "0.100.14",
26
+ "@effectstream/runtime": "0.100.14",
27
+ "@effectstream/sync": "0.100.14",
28
+ "@effectstream/utils": "0.100.14",
29
29
  "@electric-sql/pglite": "^0.3.14",
30
30
  "@pgtyped/runtime": "2.4.2",
31
31
  "@pgtyped/parser": "2.4.2",
package/src/mod.ts CHANGED
@@ -16,21 +16,27 @@ export type {
16
16
  // https://github.com/adelsz/pgtyped/issues/565
17
17
  export {
18
18
  blockHeightDone,
19
+ deleteEmptyBlocks,
19
20
  getBlockByHash,
20
21
  getBlockHeights,
21
22
  getBlockSeeds,
23
+ getLastNonEmptyBlockHash,
22
24
  getLatestProcessedBlockHeight,
23
25
  saveLastBlock,
24
26
  } from "./sql/block-heights.queries.ts";
25
27
  export type {
26
28
  IBlockHeightDoneParams,
27
29
  IBlockHeightDoneResult,
30
+ IDeleteEmptyBlocksParams,
31
+ IDeleteEmptyBlocksResult,
28
32
  IGetBlockByHashParams,
29
33
  IGetBlockByHashResult,
30
34
  IGetBlockHeightsParams,
31
35
  IGetBlockHeightsResult,
32
36
  IGetBlockSeedsParams,
33
37
  IGetBlockSeedsResult,
38
+ IGetLastNonEmptyBlockHashParams,
39
+ IGetLastNonEmptyBlockHashResult,
34
40
  IGetLatestProcessedBlockHeightParams,
35
41
  IGetLatestProcessedBlockHeightResult,
36
42
  ISaveLastBlockParams,
@@ -0,0 +1,187 @@
1
+ import { equal as assertEquals, ok as assert } from "node:assert/strict";
2
+ import { test } from "@effectstream/utils/runtime";
3
+ import {
4
+ makeTempDir,
5
+ mkdirRecursive,
6
+ readDir,
7
+ remove,
8
+ setFileTimes,
9
+ writeTextFile,
10
+ } from "@effectstream/utils/fs";
11
+ import { applyRetentionPolicy } from "./snapshot-handler.ts";
12
+
13
+ const MS_1H = 3_600_000;
14
+ const MS_24H = 24 * MS_1H;
15
+
16
+ const DEFAULT_RETENTION = {
17
+ lastDayHourly: true,
18
+ last3DaysSixHourly: true,
19
+ lastNDaysDaily: 7,
20
+ };
21
+
22
+ async function makeSnapshotDir(): Promise<string> {
23
+ return await makeTempDir({ prefix: "snapshot-retention-test-" });
24
+ }
25
+
26
+ /**
27
+ * Seeds `dir` with one file per hour for `hours` hours, mtimes descending
28
+ * from `now` (offset 0h, 1h, ..., hours-1h).
29
+ */
30
+ async function seedHourlySnapshots(
31
+ dir: string,
32
+ hours: number,
33
+ now: number,
34
+ ): Promise<void> {
35
+ for (let i = 0; i < hours; i++) {
36
+ const mtimeMs = now - i * MS_1H;
37
+ const iso = new Date(mtimeMs).toISOString().replace(/:/g, "-").replace(/\.\d{3}/, "");
38
+ const path = `${dir}/snapshot-${iso}.dump`;
39
+ await writeTextFile(path, "x");
40
+ await setFileTimes(path, mtimeMs, mtimeMs);
41
+ }
42
+ }
43
+
44
+ async function listSnapshots(dir: string): Promise<string[]> {
45
+ const out: string[] = [];
46
+ for await (const e of readDir(dir)) {
47
+ if (e.isFile && e.name.startsWith("snapshot-") && e.name.endsWith(".dump")) out.push(e.name);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ test("retention: single pass over 8 days populates all tiers", async () => {
53
+ const dir = await makeSnapshotDir();
54
+ // Align `now` to a 24h UTC boundary so bucket math is deterministic.
55
+ const now = Math.floor(Date.now() / MS_24H) * MS_24H;
56
+ await seedHourlySnapshots(dir, 8 * 24, now);
57
+
58
+ await applyRetentionPolicy(dir, DEFAULT_RETENTION, now);
59
+
60
+ const survivors = await listSnapshots(dir);
61
+ // Expected (boundary-inclusive math, `now` aligned to 24h):
62
+ // hourly tier i=0..24 (age ≤ 24h) → 25 distinct hour buckets → 25 files
63
+ // 6h tier i=25..72 (24h<age≤72h), mtimes span 48h → 8 distinct 6h bins → 8 files
64
+ // daily tier i=73..168 (72h<age≤7d), mtimes span 96h → 4 distinct day bins → 4 files
65
+ // deleted i=169..191 (age > 7d) → 0 files
66
+ assertEquals(survivors.length, 25 + 8 + 4);
67
+ });
68
+
69
+ test("retention: bucket keys are stable across successive passes (regression)", async () => {
70
+ // The bug: age-relative bucket keys slide forward every call, so the file
71
+ // kept in "bucket 6h:4" gets replaced each tick by a newer file that just
72
+ // crossed 24h. After many iterations only ~hourly files survive.
73
+ //
74
+ // With mtime-absolute bucket keys a file's bucket is fixed at write time.
75
+ // Simulate 8 days of hourly snapshots with retention running each hour and
76
+ // confirm the survivor count stays consistent with multi-tier retention.
77
+ const dir = await makeSnapshotDir();
78
+ const baseNow = Math.floor(Date.now() / MS_24H) * MS_24H;
79
+
80
+ // Simulate 8 days * 24 ticks: each tick drops a new snapshot and runs retention.
81
+ const totalTicks = 8 * 24;
82
+ for (let tick = 0; tick < totalTicks; tick++) {
83
+ const now = baseNow + tick * MS_1H;
84
+ const iso = new Date(now).toISOString().replace(/:/g, "-").replace(/\.\d{3}/, "");
85
+ const path = `${dir}/snapshot-${iso}.dump`;
86
+ await writeTextFile(path, "x");
87
+ await setFileTimes(path, now, now);
88
+ await applyRetentionPolicy(dir, DEFAULT_RETENTION, now);
89
+ }
90
+
91
+ const survivors = await listSnapshots(dir);
92
+ // With the old (buggy) bucketing this collapses to ~26 (hourly tier plus
93
+ // a handful of in-flight files). With the fix we expect ~37.
94
+ assert(
95
+ survivors.length >= 32,
96
+ `expected multi-tier retention to keep ≥32 files after 8 days of ticks, got ${survivors.length}`,
97
+ );
98
+ assert(
99
+ survivors.length <= 42,
100
+ `expected ≤42 files after retention, got ${survivors.length}`,
101
+ );
102
+ });
103
+
104
+ test("retention: lastDayHourly=false keeps all files in hourly tier", async () => {
105
+ const dir = await makeSnapshotDir();
106
+ const now = Math.floor(Date.now() / MS_24H) * MS_24H;
107
+ await seedHourlySnapshots(dir, 24, now);
108
+
109
+ await applyRetentionPolicy(
110
+ dir,
111
+ { ...DEFAULT_RETENTION, lastDayHourly: false },
112
+ now,
113
+ );
114
+
115
+ const survivors = await listSnapshots(dir);
116
+ assertEquals(survivors.length, 24);
117
+ });
118
+
119
+ test("retention: last3DaysSixHourly=false keeps all files in 24h–72h tier", async () => {
120
+ const dir = await makeSnapshotDir();
121
+ const now = Math.floor(Date.now() / MS_24H) * MS_24H;
122
+ // Seed 72 hourly files spanning 3 days.
123
+ await seedHourlySnapshots(dir, 72, now);
124
+
125
+ await applyRetentionPolicy(
126
+ dir,
127
+ { ...DEFAULT_RETENTION, last3DaysSixHourly: false },
128
+ now,
129
+ );
130
+
131
+ const survivors = await listSnapshots(dir);
132
+ // Hourly tier (i=0..24) → 25 distinct-hour keepers
133
+ // 6h tier with short-circuit → all i=25..71 kept (47 files)
134
+ assertEquals(survivors.length, 25 + 47);
135
+ });
136
+
137
+ test("retention: lastNDaysDaily=1 deletes everything past 24h", async () => {
138
+ const dir = await makeSnapshotDir();
139
+ const now = Math.floor(Date.now() / MS_24H) * MS_24H;
140
+ await seedHourlySnapshots(dir, 48, now);
141
+
142
+ await applyRetentionPolicy(
143
+ dir,
144
+ { ...DEFAULT_RETENTION, lastNDaysDaily: 1 },
145
+ now,
146
+ );
147
+
148
+ const survivors = await listSnapshots(dir);
149
+ // Only i=0..24 survive (age ≤ 24h = MS_ND). 25 distinct hour buckets.
150
+ assertEquals(survivors.length, 25);
151
+ });
152
+
153
+ test("retention: empty directory is a no-op", async () => {
154
+ const dir = await makeSnapshotDir();
155
+ await applyRetentionPolicy(dir, DEFAULT_RETENTION, Date.now());
156
+ const survivors = await listSnapshots(dir);
157
+ assertEquals(survivors.length, 0);
158
+ });
159
+
160
+ test("retention: missing directory is a no-op (warns, does not throw)", async () => {
161
+ const dir = await makeTempDir({ prefix: "snapshot-retention-missing-" });
162
+ await remove(dir, { recursive: true });
163
+ // Should not throw.
164
+ await applyRetentionPolicy(dir, DEFAULT_RETENTION, Date.now());
165
+ });
166
+
167
+ test("retention: leaves non-snapshot files untouched", async () => {
168
+ const dir = await makeSnapshotDir();
169
+ await mkdirRecursive(dir);
170
+ const now = Math.floor(Date.now() / MS_24H) * MS_24H;
171
+ // Seed an ancient snapshot that would be deleted by policy (age > 7d).
172
+ await seedHourlySnapshots(dir, 1, now - 30 * MS_24H);
173
+ // Seed unrelated files that must be left alone.
174
+ await writeTextFile(`${dir}/README.md`, "keep me");
175
+ await writeTextFile(`${dir}/snapshot-bad-name.txt`, "keep me");
176
+ await writeTextFile(`${dir}/other.dump`, "keep me");
177
+
178
+ await applyRetentionPolicy(dir, DEFAULT_RETENTION, now);
179
+
180
+ const remaining: string[] = [];
181
+ for await (const e of readDir(dir)) remaining.push(e.name);
182
+ assert(remaining.includes("README.md"));
183
+ assert(remaining.includes("snapshot-bad-name.txt"));
184
+ assert(remaining.includes("other.dump"));
185
+ // Ancient snapshot-*.dump should have been deleted.
186
+ assertEquals(remaining.filter((n) => n.startsWith("snapshot-") && n.endsWith(".dump")).length, 0);
187
+ });
@@ -1,6 +1,17 @@
1
1
  import { ENV } from "@effectstream/utils/node-env";
2
+ import { cwd } from "@effectstream/utils/runtime";
3
+ import {
4
+ mkdirRecursive,
5
+ readDir,
6
+ remove,
7
+ statMtime,
8
+ } from "@effectstream/utils/fs";
9
+ import { spawnOutput } from "@effectstream/utils/runtime-spawn";
2
10
  import { sleep, until, type Operation } from "effection";
3
11
 
12
+ /** `process.env` works under Node, Bun, and Deno (via node: compat). */
13
+ declare const process: { env: Record<string, string | undefined> };
14
+
4
15
  export interface SnapshotRetentionConfig {
5
16
  /** One per hour for last 24 h. Default: `true` */
6
17
  lastDayHourly?: boolean;
@@ -19,6 +30,39 @@ export interface SnapshotConfig {
19
30
  retention?: SnapshotRetentionConfig;
20
31
  }
21
32
 
33
+ interface ResolvedSnapshotConfig {
34
+ intervalSeconds: number;
35
+ path: string;
36
+ retention: Required<SnapshotRetentionConfig>;
37
+ }
38
+
39
+ /**
40
+ * Precedence: explicit config field > env var > hardcoded default.
41
+ * Env vars other than INTERVAL_SECONDS carry their hardcoded defaults inside
42
+ * `ENV.getConfig`, so `??` here only needs to cover INTERVAL_SECONDS (whose
43
+ * env `defaultValue` is intentionally `undefined`).
44
+ */
45
+ function resolveSnapshotConfig(config?: SnapshotConfig): ResolvedSnapshotConfig {
46
+ return {
47
+ intervalSeconds:
48
+ config?.intervalSeconds
49
+ ?? ENV.EFFECTSTREAM_SNAPSHOT_INTERVAL_SECONDS
50
+ ?? 3600,
51
+ path: config?.path ?? ENV.EFFECTSTREAM_SNAPSHOT_PATH,
52
+ retention: {
53
+ lastDayHourly:
54
+ config?.retention?.lastDayHourly
55
+ ?? ENV.EFFECTSTREAM_SNAPSHOT_LAST_DAY_HOURLY,
56
+ last3DaysSixHourly:
57
+ config?.retention?.last3DaysSixHourly
58
+ ?? ENV.EFFECTSTREAM_SNAPSHOT_LAST_3_DAYS_SIX_HOURLY,
59
+ lastNDaysDaily:
60
+ config?.retention?.lastNDaysDaily
61
+ ?? ENV.EFFECTSTREAM_SNAPSHOT_LAST_N_DAYS,
62
+ },
63
+ };
64
+ }
65
+
22
66
  /**
23
67
  * Creates a database snapshot using `pg_dump` in custom format (`-F c`).
24
68
  * Must be called AFTER releasing the DB mutex (pg_dump opens its own connection).
@@ -35,14 +79,22 @@ export async function createSnapshot(
35
79
  return;
36
80
  }
37
81
 
38
- const snapshotDir = config?.path ?? "./snapshots";
82
+ const resolved = resolveSnapshotConfig(config);
83
+ const snapshotDir = resolved.path;
39
84
  const timestamp = new Date().toISOString().replace(/:/g, "-").replace(/\.\d{3}/, "");
40
85
  const snapshotPath = `${snapshotDir}/snapshot-${timestamp}.dump`;
41
86
 
42
- await Deno.mkdir(snapshotDir, { recursive: true });
87
+ await mkdirRecursive(snapshotDir);
43
88
  console.log(`[Snapshot] Creating snapshot at ${snapshotPath}...`);
44
89
 
45
- const cmd = new Deno.Command("pg_dump", {
90
+ // Inherit the ambient env and add PGPASSWORD. `process.env` is populated on
91
+ // Node/Bun natively and on Deno via node:compat when --allow-env is set.
92
+ const ambientEnv: Record<string, string> = {};
93
+ for (const [k, v] of Object.entries(process.env)) {
94
+ if (v !== undefined) ambientEnv[k] = v;
95
+ }
96
+
97
+ const result = await spawnOutput("pg_dump", {
46
98
  args: [
47
99
  "-h", ENV.DB_HOST,
48
100
  "-p", String(ENV.DB_PORT),
@@ -51,18 +103,16 @@ export async function createSnapshot(
51
103
  "-F", "c",
52
104
  "-f", snapshotPath,
53
105
  ],
54
- env: { ...Deno.env.toObject(), PGPASSWORD: ENV.DB_PW ?? "" },
106
+ env: { ...ambientEnv, PGPASSWORD: ENV.DB_PW ?? "" },
55
107
  stdout: "inherit",
56
108
  stderr: "inherit",
57
109
  });
58
-
59
- const status = await cmd.output();
60
- if (!status.success) {
61
- throw new Error(`[Snapshot] pg_dump failed with exit code ${status.code}`);
110
+ if (!result.success) {
111
+ throw new Error(`[Snapshot] pg_dump failed with exit code ${result.code}`);
62
112
  }
63
113
 
64
114
  console.log(`[Snapshot] Snapshot created: ${snapshotPath}`);
65
- await applyRetentionPolicy(snapshotDir, config?.retention);
115
+ await applyRetentionPolicy(snapshotDir, resolved.retention);
66
116
  }
67
117
 
68
118
  /**
@@ -73,33 +123,38 @@ export async function createSnapshot(
73
123
  export function* runSnapshotLoop(
74
124
  snapshotConfig: SnapshotConfig,
75
125
  ): Operation<void> {
76
- const intervalMs = (snapshotConfig.intervalSeconds ?? 3600) * 1000;
77
- console.log(`[Snapshot] Loop started (interval ${intervalMs / 1000}s, path ${snapshotConfig.path ?? "./snapshots"}, cwd ${Deno.cwd()})`);
126
+ const resolved = resolveSnapshotConfig(snapshotConfig);
127
+ const intervalMs = resolved.intervalSeconds * 1000;
128
+ console.log(`[Snapshot] Loop started (interval ${intervalMs / 1000}s, path ${resolved.path}, cwd ${cwd()})`);
78
129
  while (true) {
130
+ yield* sleep(intervalMs);
79
131
  console.log(`[Snapshot] Firing`);
80
132
  try {
81
- yield* until(createSnapshot(snapshotConfig));
133
+ yield* until(createSnapshot(resolved));
82
134
  console.log(`[Snapshot] Done; sleeping ${intervalMs / 1000}s`);
83
135
  } catch (e) {
84
136
  console.error("[Snapshot] Failed:", e);
85
137
  }
86
- yield* sleep(intervalMs);
87
138
  }
88
139
  }
89
140
 
90
141
  /**
91
142
  * Time-based tiered retention: keeps one file per bucket (newest wins),
92
143
  * deletes everything beyond `lastNDaysDaily` days.
144
+ *
145
+ * Bucket keys are derived from the file's wall-clock mtime, not from its
146
+ * age relative to `now`. A file's bucket is therefore fixed at write time
147
+ * and does not slide forward every tick, which is what lets files actually
148
+ * age into the 6-hourly and daily tiers.
93
149
  */
94
- async function applyRetentionPolicy(
150
+ export async function applyRetentionPolicy(
95
151
  snapshotDir: string,
96
- retention?: SnapshotRetentionConfig,
152
+ retention: Required<SnapshotRetentionConfig>,
153
+ nowMs: number = Date.now(),
97
154
  ): Promise<void> {
98
- const lastDayHourly = retention?.lastDayHourly ?? true;
99
- const last3DaysSixHourly = retention?.last3DaysSixHourly ?? true;
100
- const lastNDaysDaily = retention?.lastNDaysDaily ?? 7;
155
+ const { lastDayHourly, last3DaysSixHourly, lastNDaysDaily } = retention;
101
156
 
102
- const now = Date.now();
157
+ const now = nowMs;
103
158
  const MS_1H = 3_600_000;
104
159
  const MS_24H = 24 * MS_1H;
105
160
  const MS_3D = 3 * MS_24H;
@@ -108,12 +163,12 @@ async function applyRetentionPolicy(
108
163
  const entries: { path: string; mtime: number }[] = [];
109
164
 
110
165
  try {
111
- for await (const entry of Deno.readDir(snapshotDir)) {
166
+ for await (const entry of readDir(snapshotDir)) {
112
167
  if (!entry.isFile || !entry.name.startsWith("snapshot-") || !entry.name.endsWith(".dump")) continue;
113
168
  const filePath = `${snapshotDir}/${entry.name}`;
114
169
  try {
115
- const stat = await Deno.stat(filePath);
116
- entries.push({ path: filePath, mtime: stat.mtime?.getTime() ?? 0 });
170
+ const mtime = await statMtime(filePath);
171
+ entries.push({ path: filePath, mtime });
117
172
  } catch { /* file vanished between readDir and stat */ }
118
173
  }
119
174
  } catch (e) {
@@ -137,12 +192,12 @@ async function applyRetentionPolicy(
137
192
 
138
193
  if (age <= MS_24H) {
139
194
  if (!lastDayHourly) { keepers.add(entry.path); continue; }
140
- bucketKey = `h:${Math.floor(age / MS_1H)}`;
195
+ bucketKey = `h:${Math.floor(entry.mtime / MS_1H)}`;
141
196
  } else if (age <= MS_3D) {
142
197
  if (!last3DaysSixHourly) { keepers.add(entry.path); continue; }
143
- bucketKey = `6h:${Math.floor(age / (6 * MS_1H))}`;
198
+ bucketKey = `6h:${Math.floor(entry.mtime / (6 * MS_1H))}`;
144
199
  } else {
145
- bucketKey = `d:${Math.floor(age / MS_24H)}`;
200
+ bucketKey = `d:${Math.floor(entry.mtime / MS_24H)}`;
146
201
  }
147
202
 
148
203
  if (!seenBuckets.has(bucketKey)) {
@@ -154,7 +209,7 @@ async function applyRetentionPolicy(
154
209
  for (const entry of entries) {
155
210
  if (!keepers.has(entry.path)) {
156
211
  try {
157
- await Deno.remove(entry.path);
212
+ await remove(entry.path);
158
213
  console.log(`[Snapshot] Retention: deleted ${entry.path}`);
159
214
  } catch (e) {
160
215
  console.warn(`[Snapshot] Failed to delete ${entry.path}:`, e);
@@ -174,6 +174,34 @@ const saveLastBlockIR: any = {"usedParamSet":{"block_height":true,"ver":true,"ma
174
174
  export const saveLastBlock = new PreparedQuery<ISaveLastBlockParams,ISaveLastBlockResult>(saveLastBlockIR);
175
175
 
176
176
 
177
+ /** 'GetLastNonEmptyBlockHash' parameters type */
178
+ export type IGetLastNonEmptyBlockHashParams = void;
179
+
180
+ /** 'GetLastNonEmptyBlockHash' return type */
181
+ export interface IGetLastNonEmptyBlockHashResult {
182
+ effectstream_block_hash: Buffer | null;
183
+ }
184
+
185
+ /** 'GetLastNonEmptyBlockHash' query type */
186
+ export interface IGetLastNonEmptyBlockHashQuery {
187
+ params: IGetLastNonEmptyBlockHashParams;
188
+ result: IGetLastNonEmptyBlockHashResult;
189
+ }
190
+
191
+ const getLastNonEmptyBlockHashIR: any = {"usedParamSet":{},"params":[],"statement":"SELECT effectstream_block_hash FROM effectstream.effectstream_blocks\nWHERE effectstream_block_hash IS NOT NULL\n AND effectstream_block_hash != '\\x307830'::bytea\nORDER BY block_height DESC\nLIMIT 1"};
192
+
193
+ /**
194
+ * Query generated from SQL:
195
+ * ```
196
+ * SELECT effectstream_block_hash FROM effectstream.effectstream_blocks
197
+ * WHERE effectstream_block_hash IS NOT NULL
198
+ * AND effectstream_block_hash != '\x307830'::bytea
199
+ * ORDER BY block_height DESC
200
+ * LIMIT 1
201
+ * ```
202
+ */
203
+ export const getLastNonEmptyBlockHash = new PreparedQuery<IGetLastNonEmptyBlockHashParams,IGetLastNonEmptyBlockHashResult>(getLastNonEmptyBlockHashIR);
204
+
177
205
  /** 'BlockHeightDone' parameters type */
178
206
  export interface IBlockHeightDoneParams {
179
207
  block_hash: Buffer;
@@ -189,6 +217,29 @@ export interface IBlockHeightDoneQuery {
189
217
  result: IBlockHeightDoneResult;
190
218
  }
191
219
 
220
+ /** 'DeleteEmptyBlocks' parameters type */
221
+ export type IDeleteEmptyBlocksParams = void;
222
+
223
+ /** 'DeleteEmptyBlocks' return type */
224
+ export type IDeleteEmptyBlocksResult = void;
225
+
226
+ /** 'DeleteEmptyBlocks' query type */
227
+ export interface IDeleteEmptyBlocksQuery {
228
+ params: IDeleteEmptyBlocksParams;
229
+ result: IDeleteEmptyBlocksResult;
230
+ }
231
+
232
+ const deleteEmptyBlocksIR: any = {"usedParamSet":{},"params":[],"statement":"DELETE FROM effectstream.effectstream_blocks\nWHERE effectstream_block_hash = '\\x307830'::bytea"};
233
+
234
+ /**
235
+ * Query generated from SQL:
236
+ * ```
237
+ * DELETE FROM effectstream.effectstream_blocks
238
+ * WHERE effectstream_block_hash = '\x307830'::bytea
239
+ * ```
240
+ */
241
+ export const deleteEmptyBlocks = new PreparedQuery<IDeleteEmptyBlocksParams,IDeleteEmptyBlocksResult>(deleteEmptyBlocksIR);
242
+
192
243
  const blockHeightDoneIR: any = {"usedParamSet":{"block_hash":true,"block_height":true},"params":[{"name":"block_hash","required":true,"transform":{"type":"scalar"},"locs":[{"a":70,"b":81}]},{"name":"block_height","required":true,"transform":{"type":"scalar"},"locs":[{"a":104,"b":117}]}],"statement":"UPDATE effectstream.effectstream_blocks\nSET\neffectstream_block_hash = :block_hash!\nWHERE block_height = :block_height!"};
193
244
 
194
245
  /**
@@ -38,6 +38,17 @@ seed = EXCLUDED.seed,
38
38
  ms_timestamp = EXCLUDED.ms_timestamp,
39
39
  effectstream_block_hash = EXCLUDED.effectstream_block_hash;
40
40
 
41
+ /* @name getLastNonEmptyBlockHash */
42
+ SELECT effectstream_block_hash FROM effectstream.effectstream_blocks
43
+ WHERE effectstream_block_hash IS NOT NULL
44
+ AND effectstream_block_hash != '\x307830'::bytea
45
+ ORDER BY block_height DESC
46
+ LIMIT 1;
47
+
48
+ /* @name deleteEmptyBlocks */
49
+ DELETE FROM effectstream.effectstream_blocks
50
+ WHERE effectstream_block_hash = '\x307830'::bytea;
51
+
41
52
  /* @name blockHeightDone */
42
53
  UPDATE effectstream.effectstream_blocks
43
54
  SET