@hot-updater/postgres 1.0.0-rc.14 → 1.0.0-rc.17
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/index.cjs +15 -296
- package/dist/index.d.cts +7 -1
- package/dist/index.d.mts +7 -1
- package/dist/index.mjs +16 -297
- package/package.json +5 -4
- package/sql/bundles.sql +77 -189
package/dist/index.cjs
CHANGED
|
@@ -21,309 +21,28 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
let
|
|
25
|
-
let
|
|
24
|
+
let _hot_updater_server_adapters_kysely = require("@hot-updater/server/adapters/kysely");
|
|
25
|
+
let _hot_updater_server_database = require("@hot-updater/server/database");
|
|
26
26
|
let kysely = require("kysely");
|
|
27
27
|
let pg = require("pg");
|
|
28
28
|
pg = __toESM(pg, 1);
|
|
29
|
-
//#region src/postgresQuery.ts
|
|
30
|
-
const applyOrder = (order, clause) => {
|
|
31
|
-
const directed = clause.direction === "asc" ? order.asc() : order.desc();
|
|
32
|
-
return clause.nulls === void 0 ? directed : clause.nulls === "first" ? directed.nullsFirst() : directed.nullsLast();
|
|
33
|
-
};
|
|
34
|
-
const countPostgresRows = async (db, input, where) => {
|
|
35
|
-
switch (input.model) {
|
|
36
|
-
case "bundles": {
|
|
37
|
-
let rows = db.selectFrom("bundles");
|
|
38
|
-
if (where !== void 0) rows = rows.where(where);
|
|
39
|
-
const query = input.distinct === void 0 ? rows.select(({ fn }) => fn.countAll().as("count")) : db.selectFrom(rows.select(input.distinct).distinct().as("distinct_rows")).select(({ fn }) => fn.countAll().as("count"));
|
|
40
|
-
return Number((await query.executeTakeFirstOrThrow()).count);
|
|
41
|
-
}
|
|
42
|
-
case "bundle_patches": {
|
|
43
|
-
let rows = db.selectFrom("bundle_patches");
|
|
44
|
-
if (where !== void 0) rows = rows.where(where);
|
|
45
|
-
const query = input.distinct === void 0 ? rows.select(({ fn }) => fn.countAll().as("count")) : db.selectFrom(rows.select(input.distinct).distinct().as("distinct_rows")).select(({ fn }) => fn.countAll().as("count"));
|
|
46
|
-
return Number((await query.executeTakeFirstOrThrow()).count);
|
|
47
|
-
}
|
|
48
|
-
case "releases": {
|
|
49
|
-
let rows = db.selectFrom("releases");
|
|
50
|
-
if (where !== void 0) rows = rows.where(where);
|
|
51
|
-
const query = rows.select(({ fn }) => fn.countAll().as("count"));
|
|
52
|
-
return Number((await query.executeTakeFirstOrThrow()).count);
|
|
53
|
-
}
|
|
54
|
-
case "bundle_events": {
|
|
55
|
-
let rows = db.selectFrom("bundle_events");
|
|
56
|
-
if (where !== void 0) rows = rows.where(where);
|
|
57
|
-
return Number((await rows.select(({ fn }) => fn.countAll().as("count")).executeTakeFirstOrThrow()).count);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
const findManyPostgresRows = async (db, input, where) => {
|
|
62
|
-
switch (input.model) {
|
|
63
|
-
case "bundles": {
|
|
64
|
-
let query = db.selectFrom("bundles").selectAll();
|
|
65
|
-
if (where !== void 0) query = query.where(where);
|
|
66
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
67
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
68
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
69
|
-
}
|
|
70
|
-
case "release_catalogs": {
|
|
71
|
-
let query = db.selectFrom("release_catalogs").selectAll();
|
|
72
|
-
if (where !== void 0) query = query.where(where);
|
|
73
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
74
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
75
|
-
}
|
|
76
|
-
case "bundle_events": {
|
|
77
|
-
let query = db.selectFrom("bundle_events").selectAll();
|
|
78
|
-
if (where !== void 0) query = query.where(where);
|
|
79
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
80
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
81
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
82
|
-
}
|
|
83
|
-
case "channels": {
|
|
84
|
-
let query = db.selectFrom("channels").selectAll();
|
|
85
|
-
if (where !== void 0) query = query.where(where);
|
|
86
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
87
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
88
|
-
}
|
|
89
|
-
case "api_keys": {
|
|
90
|
-
let query = db.selectFrom("api_keys").selectAll();
|
|
91
|
-
if (where !== void 0) query = query.where(where);
|
|
92
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
93
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
94
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
95
|
-
}
|
|
96
|
-
case "bundle_patches": {
|
|
97
|
-
let query = db.selectFrom("bundle_patches").selectAll();
|
|
98
|
-
if (where !== void 0) query = query.where(where);
|
|
99
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
100
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
101
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
102
|
-
}
|
|
103
|
-
case "releases": {
|
|
104
|
-
let query = db.selectFrom("releases").selectAll();
|
|
105
|
-
if (where !== void 0) query = query.where(where);
|
|
106
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
107
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
//#endregion
|
|
112
29
|
//#region src/postgres.ts
|
|
113
30
|
const { Pool } = pg.default;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const stringPredicate = (condition, operator) => {
|
|
120
|
-
if (typeof condition.value !== "string") throw new InvalidPostgresPredicateError();
|
|
121
|
-
const column = kysely.sql.ref(condition.field);
|
|
122
|
-
const literal = escapeLikePattern(condition.value);
|
|
123
|
-
const pattern = operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`;
|
|
124
|
-
return "mode" in condition && condition.mode === "insensitive" ? kysely.sql`lower(${column}) like lower(${pattern}) escape '\\'` : kysely.sql`${column} like ${pattern} escape '\\'`;
|
|
125
|
-
};
|
|
126
|
-
const predicate = (condition) => {
|
|
127
|
-
const column = kysely.sql.ref(condition.field);
|
|
128
|
-
const operator = condition.operator ?? "eq";
|
|
129
|
-
switch (operator) {
|
|
130
|
-
case "eq":
|
|
131
|
-
case "ne":
|
|
132
|
-
if (condition.value === null) return operator === "eq" ? kysely.sql`${column} is null` : kysely.sql`${column} is not null`;
|
|
133
|
-
if ("mode" in condition && condition.mode === "insensitive") return operator === "eq" ? kysely.sql`lower(${column}) = lower(${condition.value})` : kysely.sql`lower(${column}) <> lower(${condition.value})`;
|
|
134
|
-
return operator === "eq" ? kysely.sql`${column} = ${condition.value}` : kysely.sql`${column} <> ${condition.value}`;
|
|
135
|
-
case "gt": return kysely.sql`${column} > ${condition.value}`;
|
|
136
|
-
case "gte": return kysely.sql`${column} >= ${condition.value}`;
|
|
137
|
-
case "lt": return kysely.sql`${column} < ${condition.value}`;
|
|
138
|
-
case "lte": return kysely.sql`${column} <= ${condition.value}`;
|
|
139
|
-
case "in":
|
|
140
|
-
case "not_in":
|
|
141
|
-
if (!Array.isArray(condition.value)) throw new InvalidPostgresPredicateError();
|
|
142
|
-
if (condition.value.length === 0) return kysely.sql`${operator === "not_in"}`;
|
|
143
|
-
return operator === "in" ? kysely.sql`${column} in (${kysely.sql.join(condition.value)})` : kysely.sql`${column} not in (${kysely.sql.join(condition.value)})`;
|
|
144
|
-
case "contains":
|
|
145
|
-
case "starts_with":
|
|
146
|
-
case "ends_with": return stringPredicate(condition, operator);
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
const buildWhere = (where) => {
|
|
150
|
-
const [first, ...rest] = where ?? [];
|
|
151
|
-
if (first === void 0) return;
|
|
152
|
-
let expression = predicate(first);
|
|
153
|
-
for (const condition of rest) {
|
|
154
|
-
const next = predicate(condition);
|
|
155
|
-
expression = condition.connector === "OR" ? kysely.sql`(${expression} or ${next})` : kysely.sql`(${expression} and ${next})`;
|
|
156
|
-
}
|
|
157
|
-
return expression;
|
|
158
|
-
};
|
|
159
|
-
const createPostgresImplementation = (db) => ({
|
|
160
|
-
async recordInsights({ event }) {
|
|
161
|
-
await db.transaction().execute(async (transaction) => {
|
|
162
|
-
await transaction.insertInto("bundle_events").values(event).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
163
|
-
await kysely.sql`INSERT INTO bundle_event_heads (install_id, id, received_at_ms, user_id, platform, channel, type, from_bundle_id, to_bundle_id)
|
|
164
|
-
SELECT install_id, id, received_at_ms, user_id, platform, channel, type, from_bundle_id, to_bundle_id
|
|
165
|
-
FROM bundle_events WHERE id = ${event.id}
|
|
166
|
-
ON CONFLICT(install_id) DO UPDATE SET
|
|
167
|
-
id = excluded.id, received_at_ms = excluded.received_at_ms, user_id = excluded.user_id,
|
|
168
|
-
platform = excluded.platform, channel = excluded.channel, type = excluded.type,
|
|
169
|
-
from_bundle_id = excluded.from_bundle_id, to_bundle_id = excluded.to_bundle_id
|
|
170
|
-
WHERE (excluded.received_at_ms, excluded.id) > (bundle_event_heads.received_at_ms, bundle_event_heads.id)`.execute(transaction);
|
|
171
|
-
});
|
|
172
|
-
},
|
|
173
|
-
async findLatestInsightsEvents(input) {
|
|
174
|
-
const where = buildWhere((0, _hot_updater_plugin_core_internal.latestInsightsWhere)(input));
|
|
175
|
-
return (await kysely.sql`SELECT event.* FROM (SELECT id, install_id FROM bundle_event_heads WHERE ${where} ORDER BY install_id ASC LIMIT ${"installId" in input ? 1 : input.limit}) AS head JOIN bundle_events AS event ON event.id = head.id ORDER BY head.install_id ASC`.execute(db)).rows;
|
|
176
|
-
},
|
|
177
|
-
async countLatestInsightsEvents(input) {
|
|
178
|
-
const where = kysely.sql`(${kysely.sql.join((0, _hot_updater_plugin_core_internal.latestInsightsCountGroups)(input).map((where) => kysely.sql`(${buildWhere(where)})`), kysely.sql` OR `)})`;
|
|
179
|
-
const result = await kysely.sql`SELECT COUNT(*) AS count FROM bundle_event_heads WHERE ${where}`.execute(db);
|
|
180
|
-
return Number(result.rows[0]?.count ?? 0);
|
|
181
|
-
},
|
|
182
|
-
async create(input) {
|
|
183
|
-
switch (input.model) {
|
|
184
|
-
case "bundles": return db.insertInto("bundles").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
185
|
-
case "bundle_patches": return db.insertInto("bundle_patches").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
186
|
-
case "bundle_events": return db.insertInto("bundle_events").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
187
|
-
case "releases": return db.insertInto("releases").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
188
|
-
case "release_catalogs": return db.insertInto("release_catalogs").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
189
|
-
case "channels": {
|
|
190
|
-
const query = db.insertInto("channels").values(input.data);
|
|
191
|
-
return await (input.onConflict === "ignore" ? query.onConflict((conflict) => conflict.column("name").doNothing()) : query).returningAll().executeTakeFirst() ?? await db.selectFrom("channels").selectAll().where("name", "=", input.data.name).executeTakeFirstOrThrow();
|
|
192
|
-
}
|
|
193
|
-
case "api_keys": return await (input.onConflict === "ignore" ? db.insertInto("api_keys").values(input.data).onConflict((conflict) => conflict.column("hash").doNothing()) : db.insertInto("api_keys").values(input.data)).returningAll().executeTakeFirst() ?? await db.selectFrom("api_keys").selectAll().where("hash", "=", input.data.hash).executeTakeFirstOrThrow();
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
async update(input) {
|
|
197
|
-
const where = buildWhere(input.where);
|
|
198
|
-
if (input.model === "api_keys") {
|
|
199
|
-
let query = db.updateTable("api_keys").set(input.update);
|
|
200
|
-
if (where !== void 0) query = query.where(where);
|
|
201
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
202
|
-
}
|
|
203
|
-
if (input.model === "releases") {
|
|
204
|
-
let query = db.updateTable("releases").set(input.update);
|
|
205
|
-
if (where !== void 0) query = query.where(where);
|
|
206
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
207
|
-
}
|
|
208
|
-
if (input.model === "release_catalogs") {
|
|
209
|
-
let query = db.updateTable("release_catalogs").set(input.update);
|
|
210
|
-
if (where !== void 0) query = query.where(where);
|
|
211
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
212
|
-
}
|
|
213
|
-
let query = db.updateTable("bundles").set(input.update);
|
|
214
|
-
if (where !== void 0) query = query.where(where);
|
|
215
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
216
|
-
},
|
|
217
|
-
async delete(input) {
|
|
218
|
-
const where = buildWhere(input.where);
|
|
219
|
-
switch (input.model) {
|
|
220
|
-
case "bundles": {
|
|
221
|
-
let query = db.deleteFrom("bundles");
|
|
222
|
-
if (where !== void 0) query = query.where(where);
|
|
223
|
-
await query.execute();
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
case "bundle_patches": {
|
|
227
|
-
let query = db.deleteFrom("bundle_patches");
|
|
228
|
-
if (where !== void 0) query = query.where(where);
|
|
229
|
-
await query.execute();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
case "channels": {
|
|
233
|
-
let query = db.deleteFrom("channels");
|
|
234
|
-
if (where !== void 0) query = query.where(where);
|
|
235
|
-
try {
|
|
236
|
-
await query.execute();
|
|
237
|
-
} catch (error) {
|
|
238
|
-
if (isForeignKeyViolation(error)) throw new _hot_updater_plugin_core_internal.DatabaseRowReferencedError();
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
case "releases": {
|
|
243
|
-
let query = db.deleteFrom("releases");
|
|
244
|
-
if (where !== void 0) query = query.where(where);
|
|
245
|
-
await query.execute();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
count: (input) => countPostgresRows(db, input, buildWhere(input.where)),
|
|
250
|
-
async findOne(input) {
|
|
251
|
-
const where = buildWhere(input.where);
|
|
252
|
-
switch (input.model) {
|
|
253
|
-
case "bundles": {
|
|
254
|
-
let query = db.selectFrom("bundles").selectAll();
|
|
255
|
-
if (where !== void 0) query = query.where(where);
|
|
256
|
-
return await query.executeTakeFirst() ?? null;
|
|
257
|
-
}
|
|
258
|
-
case "api_keys": {
|
|
259
|
-
let query = db.selectFrom("api_keys").selectAll();
|
|
260
|
-
if (where !== void 0) query = query.where(where);
|
|
261
|
-
return await query.executeTakeFirst() ?? null;
|
|
262
|
-
}
|
|
263
|
-
case "bundle_patches": {
|
|
264
|
-
let query = db.selectFrom("bundle_patches").selectAll();
|
|
265
|
-
if (where !== void 0) query = query.where(where);
|
|
266
|
-
return await query.executeTakeFirst() ?? null;
|
|
267
|
-
}
|
|
268
|
-
case "channels": {
|
|
269
|
-
let query = db.selectFrom("channels").selectAll();
|
|
270
|
-
if (where !== void 0) query = query.where(where);
|
|
271
|
-
return await query.executeTakeFirst() ?? null;
|
|
272
|
-
}
|
|
273
|
-
case "releases": {
|
|
274
|
-
let query = db.selectFrom("releases").selectAll();
|
|
275
|
-
if (where !== void 0) query = query.where(where);
|
|
276
|
-
return await query.executeTakeFirst() ?? null;
|
|
277
|
-
}
|
|
278
|
-
case "release_catalogs": {
|
|
279
|
-
let query = db.selectFrom("release_catalogs").selectAll();
|
|
280
|
-
if (where !== void 0) query = query.where(where);
|
|
281
|
-
return await query.executeTakeFirst() ?? null;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
findMany: (input) => findManyPostgresRows(db, input, buildWhere(input.where)),
|
|
286
|
-
async insertChannel({ row }) {
|
|
287
|
-
const inserted = await db.insertInto("channels").values(row).onConflict((conflict) => conflict.column("name").doNothing()).returningAll().executeTakeFirst();
|
|
288
|
-
if (inserted !== void 0) return {
|
|
289
|
-
row: inserted,
|
|
290
|
-
inserted: true
|
|
291
|
-
};
|
|
292
|
-
return {
|
|
293
|
-
row: await db.selectFrom("channels").selectAll().where("name", "=", row.name).executeTakeFirstOrThrow(),
|
|
294
|
-
inserted: false
|
|
295
|
-
};
|
|
296
|
-
},
|
|
297
|
-
async deleteChannel({ id }) {
|
|
298
|
-
try {
|
|
299
|
-
return await db.deleteFrom("channels").where("id", "=", id).returning("id").executeTakeFirst() === void 0 ? {
|
|
300
|
-
deleted: false,
|
|
301
|
-
reason: "not_found"
|
|
302
|
-
} : { deleted: true };
|
|
303
|
-
} catch (error) {
|
|
304
|
-
if (isForeignKeyViolation(error)) return {
|
|
305
|
-
deleted: false,
|
|
306
|
-
reason: "not_empty"
|
|
307
|
-
};
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
transaction: (callback) => db.transaction().execute((transaction) => callback(createPostgresImplementation(transaction))),
|
|
312
|
-
dispose: () => db.destroy()
|
|
313
|
-
});
|
|
31
|
+
/**
|
|
32
|
+
* Hot Updater's database on PostgreSQL, through a `pg` pool or the given
|
|
33
|
+
* Kysely dialect. Apply `sql/bundles.sql` before first use; the schema fence
|
|
34
|
+
* refuses a database without its settings rows.
|
|
35
|
+
*/
|
|
314
36
|
const postgres = (config) => {
|
|
315
37
|
const { dialect, ...poolConfig } = config;
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
commit: adapter.commit,
|
|
325
|
-
dispose: adapter.dispose
|
|
326
|
-
});
|
|
38
|
+
const db = new kysely.Kysely({ dialect: dialect ?? new kysely.PostgresDialect({ pool: new Pool(poolConfig) }) });
|
|
39
|
+
return {
|
|
40
|
+
...(0, _hot_updater_server_database.createEngineDatabase)({
|
|
41
|
+
name: "postgres",
|
|
42
|
+
adapter: (0, _hot_updater_server_database.createSqlAdapter)({ executor: (0, _hot_updater_server_adapters_kysely.kyselyExecutor)(db, "postgresql") })
|
|
43
|
+
}),
|
|
44
|
+
dispose: () => db.destroy()
|
|
45
|
+
};
|
|
327
46
|
};
|
|
328
47
|
//#endregion
|
|
329
48
|
exports.postgres = postgres;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { EngineDatabase } from "@hot-updater/plugin-core";
|
|
1
2
|
import { Dialect } from "kysely";
|
|
2
3
|
import { PoolConfig } from "pg";
|
|
3
4
|
//#region src/postgres.d.ts
|
|
4
5
|
type PostgresConfig = PoolConfig & {
|
|
5
6
|
readonly dialect?: Dialect;
|
|
6
7
|
};
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Hot Updater's database on PostgreSQL, through a `pg` pool or the given
|
|
10
|
+
* Kysely dialect. Apply `sql/bundles.sql` before first use; the schema fence
|
|
11
|
+
* refuses a database without its settings rows.
|
|
12
|
+
*/
|
|
13
|
+
declare const postgres: (config: PostgresConfig) => EngineDatabase;
|
|
8
14
|
//#endregion
|
|
9
15
|
export { PostgresConfig, postgres };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Dialect } from "kysely";
|
|
2
2
|
import { PoolConfig } from "pg";
|
|
3
|
+
import { EngineDatabase } from "@hot-updater/plugin-core";
|
|
3
4
|
//#region src/postgres.d.ts
|
|
4
5
|
type PostgresConfig = PoolConfig & {
|
|
5
6
|
readonly dialect?: Dialect;
|
|
6
7
|
};
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Hot Updater's database on PostgreSQL, through a `pg` pool or the given
|
|
10
|
+
* Kysely dialect. Apply `sql/bundles.sql` before first use; the schema fence
|
|
11
|
+
* refuses a database without its settings rows.
|
|
12
|
+
*/
|
|
13
|
+
declare const postgres: (config: PostgresConfig) => EngineDatabase;
|
|
8
14
|
//#endregion
|
|
9
15
|
export { PostgresConfig, postgres };
|
package/dist/index.mjs
CHANGED
|
@@ -1,305 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Kysely, PostgresDialect
|
|
1
|
+
import { kyselyExecutor } from "@hot-updater/server/adapters/kysely";
|
|
2
|
+
import { createEngineDatabase, createSqlAdapter } from "@hot-updater/server/database";
|
|
3
|
+
import { Kysely, PostgresDialect } from "kysely";
|
|
4
4
|
import pg from "pg";
|
|
5
|
-
//#region src/postgresQuery.ts
|
|
6
|
-
const applyOrder = (order, clause) => {
|
|
7
|
-
const directed = clause.direction === "asc" ? order.asc() : order.desc();
|
|
8
|
-
return clause.nulls === void 0 ? directed : clause.nulls === "first" ? directed.nullsFirst() : directed.nullsLast();
|
|
9
|
-
};
|
|
10
|
-
const countPostgresRows = async (db, input, where) => {
|
|
11
|
-
switch (input.model) {
|
|
12
|
-
case "bundles": {
|
|
13
|
-
let rows = db.selectFrom("bundles");
|
|
14
|
-
if (where !== void 0) rows = rows.where(where);
|
|
15
|
-
const query = input.distinct === void 0 ? rows.select(({ fn }) => fn.countAll().as("count")) : db.selectFrom(rows.select(input.distinct).distinct().as("distinct_rows")).select(({ fn }) => fn.countAll().as("count"));
|
|
16
|
-
return Number((await query.executeTakeFirstOrThrow()).count);
|
|
17
|
-
}
|
|
18
|
-
case "bundle_patches": {
|
|
19
|
-
let rows = db.selectFrom("bundle_patches");
|
|
20
|
-
if (where !== void 0) rows = rows.where(where);
|
|
21
|
-
const query = input.distinct === void 0 ? rows.select(({ fn }) => fn.countAll().as("count")) : db.selectFrom(rows.select(input.distinct).distinct().as("distinct_rows")).select(({ fn }) => fn.countAll().as("count"));
|
|
22
|
-
return Number((await query.executeTakeFirstOrThrow()).count);
|
|
23
|
-
}
|
|
24
|
-
case "releases": {
|
|
25
|
-
let rows = db.selectFrom("releases");
|
|
26
|
-
if (where !== void 0) rows = rows.where(where);
|
|
27
|
-
const query = rows.select(({ fn }) => fn.countAll().as("count"));
|
|
28
|
-
return Number((await query.executeTakeFirstOrThrow()).count);
|
|
29
|
-
}
|
|
30
|
-
case "bundle_events": {
|
|
31
|
-
let rows = db.selectFrom("bundle_events");
|
|
32
|
-
if (where !== void 0) rows = rows.where(where);
|
|
33
|
-
return Number((await rows.select(({ fn }) => fn.countAll().as("count")).executeTakeFirstOrThrow()).count);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const findManyPostgresRows = async (db, input, where) => {
|
|
38
|
-
switch (input.model) {
|
|
39
|
-
case "bundles": {
|
|
40
|
-
let query = db.selectFrom("bundles").selectAll();
|
|
41
|
-
if (where !== void 0) query = query.where(where);
|
|
42
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
43
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
44
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
45
|
-
}
|
|
46
|
-
case "release_catalogs": {
|
|
47
|
-
let query = db.selectFrom("release_catalogs").selectAll();
|
|
48
|
-
if (where !== void 0) query = query.where(where);
|
|
49
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
50
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
51
|
-
}
|
|
52
|
-
case "bundle_events": {
|
|
53
|
-
let query = db.selectFrom("bundle_events").selectAll();
|
|
54
|
-
if (where !== void 0) query = query.where(where);
|
|
55
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
56
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
57
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
58
|
-
}
|
|
59
|
-
case "channels": {
|
|
60
|
-
let query = db.selectFrom("channels").selectAll();
|
|
61
|
-
if (where !== void 0) query = query.where(where);
|
|
62
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
63
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
64
|
-
}
|
|
65
|
-
case "api_keys": {
|
|
66
|
-
let query = db.selectFrom("api_keys").selectAll();
|
|
67
|
-
if (where !== void 0) query = query.where(where);
|
|
68
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
69
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
70
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
71
|
-
}
|
|
72
|
-
case "bundle_patches": {
|
|
73
|
-
let query = db.selectFrom("bundle_patches").selectAll();
|
|
74
|
-
if (where !== void 0) query = query.where(where);
|
|
75
|
-
if (input.distinctOn !== void 0) query = query.distinctOn(input.distinctOn.fields);
|
|
76
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
77
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
78
|
-
}
|
|
79
|
-
case "releases": {
|
|
80
|
-
let query = db.selectFrom("releases").selectAll();
|
|
81
|
-
if (where !== void 0) query = query.where(where);
|
|
82
|
-
for (const clause of input.orderBy ?? []) query = query.orderBy(clause.field, (order) => applyOrder(order, clause));
|
|
83
|
-
return query.limit(input.limit).offset(input.offset).execute();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
//#endregion
|
|
88
5
|
//#region src/postgres.ts
|
|
89
6
|
const { Pool } = pg;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const stringPredicate = (condition, operator) => {
|
|
96
|
-
if (typeof condition.value !== "string") throw new InvalidPostgresPredicateError();
|
|
97
|
-
const column = sql.ref(condition.field);
|
|
98
|
-
const literal = escapeLikePattern(condition.value);
|
|
99
|
-
const pattern = operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`;
|
|
100
|
-
return "mode" in condition && condition.mode === "insensitive" ? sql`lower(${column}) like lower(${pattern}) escape '\\'` : sql`${column} like ${pattern} escape '\\'`;
|
|
101
|
-
};
|
|
102
|
-
const predicate = (condition) => {
|
|
103
|
-
const column = sql.ref(condition.field);
|
|
104
|
-
const operator = condition.operator ?? "eq";
|
|
105
|
-
switch (operator) {
|
|
106
|
-
case "eq":
|
|
107
|
-
case "ne":
|
|
108
|
-
if (condition.value === null) return operator === "eq" ? sql`${column} is null` : sql`${column} is not null`;
|
|
109
|
-
if ("mode" in condition && condition.mode === "insensitive") return operator === "eq" ? sql`lower(${column}) = lower(${condition.value})` : sql`lower(${column}) <> lower(${condition.value})`;
|
|
110
|
-
return operator === "eq" ? sql`${column} = ${condition.value}` : sql`${column} <> ${condition.value}`;
|
|
111
|
-
case "gt": return sql`${column} > ${condition.value}`;
|
|
112
|
-
case "gte": return sql`${column} >= ${condition.value}`;
|
|
113
|
-
case "lt": return sql`${column} < ${condition.value}`;
|
|
114
|
-
case "lte": return sql`${column} <= ${condition.value}`;
|
|
115
|
-
case "in":
|
|
116
|
-
case "not_in":
|
|
117
|
-
if (!Array.isArray(condition.value)) throw new InvalidPostgresPredicateError();
|
|
118
|
-
if (condition.value.length === 0) return sql`${operator === "not_in"}`;
|
|
119
|
-
return operator === "in" ? sql`${column} in (${sql.join(condition.value)})` : sql`${column} not in (${sql.join(condition.value)})`;
|
|
120
|
-
case "contains":
|
|
121
|
-
case "starts_with":
|
|
122
|
-
case "ends_with": return stringPredicate(condition, operator);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
const buildWhere = (where) => {
|
|
126
|
-
const [first, ...rest] = where ?? [];
|
|
127
|
-
if (first === void 0) return;
|
|
128
|
-
let expression = predicate(first);
|
|
129
|
-
for (const condition of rest) {
|
|
130
|
-
const next = predicate(condition);
|
|
131
|
-
expression = condition.connector === "OR" ? sql`(${expression} or ${next})` : sql`(${expression} and ${next})`;
|
|
132
|
-
}
|
|
133
|
-
return expression;
|
|
134
|
-
};
|
|
135
|
-
const createPostgresImplementation = (db) => ({
|
|
136
|
-
async recordInsights({ event }) {
|
|
137
|
-
await db.transaction().execute(async (transaction) => {
|
|
138
|
-
await transaction.insertInto("bundle_events").values(event).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
139
|
-
await sql`INSERT INTO bundle_event_heads (install_id, id, received_at_ms, user_id, platform, channel, type, from_bundle_id, to_bundle_id)
|
|
140
|
-
SELECT install_id, id, received_at_ms, user_id, platform, channel, type, from_bundle_id, to_bundle_id
|
|
141
|
-
FROM bundle_events WHERE id = ${event.id}
|
|
142
|
-
ON CONFLICT(install_id) DO UPDATE SET
|
|
143
|
-
id = excluded.id, received_at_ms = excluded.received_at_ms, user_id = excluded.user_id,
|
|
144
|
-
platform = excluded.platform, channel = excluded.channel, type = excluded.type,
|
|
145
|
-
from_bundle_id = excluded.from_bundle_id, to_bundle_id = excluded.to_bundle_id
|
|
146
|
-
WHERE (excluded.received_at_ms, excluded.id) > (bundle_event_heads.received_at_ms, bundle_event_heads.id)`.execute(transaction);
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
async findLatestInsightsEvents(input) {
|
|
150
|
-
const where = buildWhere(latestInsightsWhere(input));
|
|
151
|
-
return (await sql`SELECT event.* FROM (SELECT id, install_id FROM bundle_event_heads WHERE ${where} ORDER BY install_id ASC LIMIT ${"installId" in input ? 1 : input.limit}) AS head JOIN bundle_events AS event ON event.id = head.id ORDER BY head.install_id ASC`.execute(db)).rows;
|
|
152
|
-
},
|
|
153
|
-
async countLatestInsightsEvents(input) {
|
|
154
|
-
const where = sql`(${sql.join(latestInsightsCountGroups(input).map((where) => sql`(${buildWhere(where)})`), sql` OR `)})`;
|
|
155
|
-
const result = await sql`SELECT COUNT(*) AS count FROM bundle_event_heads WHERE ${where}`.execute(db);
|
|
156
|
-
return Number(result.rows[0]?.count ?? 0);
|
|
157
|
-
},
|
|
158
|
-
async create(input) {
|
|
159
|
-
switch (input.model) {
|
|
160
|
-
case "bundles": return db.insertInto("bundles").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
161
|
-
case "bundle_patches": return db.insertInto("bundle_patches").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
162
|
-
case "bundle_events": return db.insertInto("bundle_events").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
163
|
-
case "releases": return db.insertInto("releases").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
164
|
-
case "release_catalogs": return db.insertInto("release_catalogs").values(input.data).returningAll().executeTakeFirstOrThrow();
|
|
165
|
-
case "channels": {
|
|
166
|
-
const query = db.insertInto("channels").values(input.data);
|
|
167
|
-
return await (input.onConflict === "ignore" ? query.onConflict((conflict) => conflict.column("name").doNothing()) : query).returningAll().executeTakeFirst() ?? await db.selectFrom("channels").selectAll().where("name", "=", input.data.name).executeTakeFirstOrThrow();
|
|
168
|
-
}
|
|
169
|
-
case "api_keys": return await (input.onConflict === "ignore" ? db.insertInto("api_keys").values(input.data).onConflict((conflict) => conflict.column("hash").doNothing()) : db.insertInto("api_keys").values(input.data)).returningAll().executeTakeFirst() ?? await db.selectFrom("api_keys").selectAll().where("hash", "=", input.data.hash).executeTakeFirstOrThrow();
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
async update(input) {
|
|
173
|
-
const where = buildWhere(input.where);
|
|
174
|
-
if (input.model === "api_keys") {
|
|
175
|
-
let query = db.updateTable("api_keys").set(input.update);
|
|
176
|
-
if (where !== void 0) query = query.where(where);
|
|
177
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
178
|
-
}
|
|
179
|
-
if (input.model === "releases") {
|
|
180
|
-
let query = db.updateTable("releases").set(input.update);
|
|
181
|
-
if (where !== void 0) query = query.where(where);
|
|
182
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
183
|
-
}
|
|
184
|
-
if (input.model === "release_catalogs") {
|
|
185
|
-
let query = db.updateTable("release_catalogs").set(input.update);
|
|
186
|
-
if (where !== void 0) query = query.where(where);
|
|
187
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
188
|
-
}
|
|
189
|
-
let query = db.updateTable("bundles").set(input.update);
|
|
190
|
-
if (where !== void 0) query = query.where(where);
|
|
191
|
-
return await query.returningAll().executeTakeFirst() ?? null;
|
|
192
|
-
},
|
|
193
|
-
async delete(input) {
|
|
194
|
-
const where = buildWhere(input.where);
|
|
195
|
-
switch (input.model) {
|
|
196
|
-
case "bundles": {
|
|
197
|
-
let query = db.deleteFrom("bundles");
|
|
198
|
-
if (where !== void 0) query = query.where(where);
|
|
199
|
-
await query.execute();
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
case "bundle_patches": {
|
|
203
|
-
let query = db.deleteFrom("bundle_patches");
|
|
204
|
-
if (where !== void 0) query = query.where(where);
|
|
205
|
-
await query.execute();
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
case "channels": {
|
|
209
|
-
let query = db.deleteFrom("channels");
|
|
210
|
-
if (where !== void 0) query = query.where(where);
|
|
211
|
-
try {
|
|
212
|
-
await query.execute();
|
|
213
|
-
} catch (error) {
|
|
214
|
-
if (isForeignKeyViolation(error)) throw new DatabaseRowReferencedError();
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
case "releases": {
|
|
219
|
-
let query = db.deleteFrom("releases");
|
|
220
|
-
if (where !== void 0) query = query.where(where);
|
|
221
|
-
await query.execute();
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
count: (input) => countPostgresRows(db, input, buildWhere(input.where)),
|
|
226
|
-
async findOne(input) {
|
|
227
|
-
const where = buildWhere(input.where);
|
|
228
|
-
switch (input.model) {
|
|
229
|
-
case "bundles": {
|
|
230
|
-
let query = db.selectFrom("bundles").selectAll();
|
|
231
|
-
if (where !== void 0) query = query.where(where);
|
|
232
|
-
return await query.executeTakeFirst() ?? null;
|
|
233
|
-
}
|
|
234
|
-
case "api_keys": {
|
|
235
|
-
let query = db.selectFrom("api_keys").selectAll();
|
|
236
|
-
if (where !== void 0) query = query.where(where);
|
|
237
|
-
return await query.executeTakeFirst() ?? null;
|
|
238
|
-
}
|
|
239
|
-
case "bundle_patches": {
|
|
240
|
-
let query = db.selectFrom("bundle_patches").selectAll();
|
|
241
|
-
if (where !== void 0) query = query.where(where);
|
|
242
|
-
return await query.executeTakeFirst() ?? null;
|
|
243
|
-
}
|
|
244
|
-
case "channels": {
|
|
245
|
-
let query = db.selectFrom("channels").selectAll();
|
|
246
|
-
if (where !== void 0) query = query.where(where);
|
|
247
|
-
return await query.executeTakeFirst() ?? null;
|
|
248
|
-
}
|
|
249
|
-
case "releases": {
|
|
250
|
-
let query = db.selectFrom("releases").selectAll();
|
|
251
|
-
if (where !== void 0) query = query.where(where);
|
|
252
|
-
return await query.executeTakeFirst() ?? null;
|
|
253
|
-
}
|
|
254
|
-
case "release_catalogs": {
|
|
255
|
-
let query = db.selectFrom("release_catalogs").selectAll();
|
|
256
|
-
if (where !== void 0) query = query.where(where);
|
|
257
|
-
return await query.executeTakeFirst() ?? null;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
findMany: (input) => findManyPostgresRows(db, input, buildWhere(input.where)),
|
|
262
|
-
async insertChannel({ row }) {
|
|
263
|
-
const inserted = await db.insertInto("channels").values(row).onConflict((conflict) => conflict.column("name").doNothing()).returningAll().executeTakeFirst();
|
|
264
|
-
if (inserted !== void 0) return {
|
|
265
|
-
row: inserted,
|
|
266
|
-
inserted: true
|
|
267
|
-
};
|
|
268
|
-
return {
|
|
269
|
-
row: await db.selectFrom("channels").selectAll().where("name", "=", row.name).executeTakeFirstOrThrow(),
|
|
270
|
-
inserted: false
|
|
271
|
-
};
|
|
272
|
-
},
|
|
273
|
-
async deleteChannel({ id }) {
|
|
274
|
-
try {
|
|
275
|
-
return await db.deleteFrom("channels").where("id", "=", id).returning("id").executeTakeFirst() === void 0 ? {
|
|
276
|
-
deleted: false,
|
|
277
|
-
reason: "not_found"
|
|
278
|
-
} : { deleted: true };
|
|
279
|
-
} catch (error) {
|
|
280
|
-
if (isForeignKeyViolation(error)) return {
|
|
281
|
-
deleted: false,
|
|
282
|
-
reason: "not_empty"
|
|
283
|
-
};
|
|
284
|
-
throw error;
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
transaction: (callback) => db.transaction().execute((transaction) => callback(createPostgresImplementation(transaction))),
|
|
288
|
-
dispose: () => db.destroy()
|
|
289
|
-
});
|
|
7
|
+
/**
|
|
8
|
+
* Hot Updater's database on PostgreSQL, through a `pg` pool or the given
|
|
9
|
+
* Kysely dialect. Apply `sql/bundles.sql` before first use; the schema fence
|
|
10
|
+
* refuses a database without its settings rows.
|
|
11
|
+
*/
|
|
290
12
|
const postgres = (config) => {
|
|
291
13
|
const { dialect, ...poolConfig } = config;
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
commit: adapter.commit,
|
|
301
|
-
dispose: adapter.dispose
|
|
302
|
-
});
|
|
14
|
+
const db = new Kysely({ dialect: dialect ?? new PostgresDialect({ pool: new Pool(poolConfig) }) });
|
|
15
|
+
return {
|
|
16
|
+
...createEngineDatabase({
|
|
17
|
+
name: "postgres",
|
|
18
|
+
adapter: createSqlAdapter({ executor: kyselyExecutor(db, "postgresql") })
|
|
19
|
+
}),
|
|
20
|
+
dispose: () => db.destroy()
|
|
21
|
+
};
|
|
303
22
|
};
|
|
304
23
|
//#endregion
|
|
305
24
|
export { postgres };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/postgres",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.17",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"kysely": "0.28.17",
|
|
36
36
|
"pg": "8.13.1",
|
|
37
|
-
"@hot-updater/core": "1.0.0-rc.
|
|
38
|
-
"@hot-updater/
|
|
37
|
+
"@hot-updater/plugin-core": "1.0.0-rc.16",
|
|
38
|
+
"@hot-updater/core": "1.0.0-rc.15",
|
|
39
|
+
"@hot-updater/server": "1.0.0-rc.17"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@electric-sql/pglite": "0.4.1",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"camelcase-keys": "^9.1.3",
|
|
45
46
|
"kysely-pglite-dialect": "1.2.0",
|
|
46
47
|
"pg-minify": "^1.6.5",
|
|
47
|
-
"@hot-updater/test-utils": "1.0.0-rc.
|
|
48
|
+
"@hot-updater/test-utils": "1.0.0-rc.16"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "tsdown",
|
package/sql/bundles.sql
CHANGED
|
@@ -1,191 +1,79 @@
|
|
|
1
1
|
-- HotUpdater.schema
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
app_version text not null,
|
|
81
|
-
channel text collate "C" not null,
|
|
82
|
-
metadata JSONB NOT NULL,
|
|
83
|
-
received_at_ms double precision not null
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
create table bundle_event_heads (
|
|
88
|
-
install_id text collate "C" primary key not null,
|
|
89
|
-
id uuid not null,
|
|
90
|
-
received_at_ms double precision not null,
|
|
91
|
-
user_id text collate "C",
|
|
92
|
-
platform text collate "C" not null,
|
|
93
|
-
channel text collate "C" not null,
|
|
94
|
-
type text not null,
|
|
95
|
-
from_bundle_id uuid,
|
|
96
|
-
to_bundle_id uuid not null
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
create table api_keys (
|
|
100
|
-
id varchar(255) primary key not null,
|
|
101
|
-
hash text not null,
|
|
102
|
-
name text not null,
|
|
103
|
-
prefix text not null,
|
|
104
|
-
role text not null,
|
|
105
|
-
created_at_ms double precision not null,
|
|
106
|
-
revoked_at_ms double precision
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
create table private_hot_updater_settings (
|
|
110
|
-
key varchar(255) primary key not null,
|
|
111
|
-
value text not null default '1.0.0'
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
create unique index channels_name_key on channels(name);
|
|
115
|
-
create index bundle_patches_bundle_id_idx on bundle_patches(bundle_id);
|
|
116
|
-
create index bundle_patches_base_bundle_id_idx on bundle_patches(base_bundle_id);
|
|
117
|
-
create index releases_scope_order_idx on releases(scope_key, id);
|
|
118
|
-
create index releases_channel_platform_order_idx on releases(channel_id, platform, id);
|
|
119
|
-
create index releases_bundle_id_idx on releases(bundle_id);
|
|
120
|
-
create index releases_fingerprint_hash_idx on releases(fingerprint_hash);
|
|
121
|
-
create index releases_enabled_idx on releases(enabled);
|
|
122
|
-
create index release_catalogs_channel_idx on release_catalogs(channel_id);
|
|
123
|
-
create index bundle_events_received_at_idx on bundle_events(received_at_ms, id);
|
|
124
|
-
create index bundle_events_install_idx on bundle_events(install_id, type, received_at_ms, id);
|
|
125
|
-
create index bundle_events_from_bundle_idx on bundle_events(type, platform, channel, from_bundle_id, received_at_ms, id);
|
|
126
|
-
create index bundle_events_to_bundle_idx on bundle_events(type, platform, channel, to_bundle_id, received_at_ms, id);
|
|
127
|
-
create unique index api_keys_hash_key on api_keys(hash);
|
|
128
|
-
create index api_keys_created_at_idx on api_keys(created_at_ms, id);
|
|
129
|
-
|
|
130
|
-
alter table channels add constraint channels_id_length_check
|
|
131
|
-
check (char_length(id) between 1 and 255);
|
|
132
|
-
alter table channels add constraint channels_name_length_check
|
|
133
|
-
check (char_length(name) between 1 and 255);
|
|
134
|
-
alter table bundles add constraint bundles_archive_byte_size_check
|
|
135
|
-
check (archive_byte_size >= 0 and archive_byte_size <= 9007199254740991);
|
|
136
|
-
alter table bundle_patches add constraint bundle_patches_byte_size_check
|
|
137
|
-
check (byte_size >= 0 and byte_size <= 9007199254740991);
|
|
138
|
-
alter table releases add constraint releases_revision_check
|
|
139
|
-
check (revision >= 1);
|
|
140
|
-
alter table releases add constraint releases_kind_bundle_check
|
|
141
|
-
check ((kind = 'BUNDLE' and bundle_id is not null) or (kind = 'EMBEDDED' and bundle_id is null));
|
|
142
|
-
alter table releases add constraint releases_strategy_target_check
|
|
143
|
-
check ((strategy = 'APP_VERSION' and target_app_version is not null and fingerprint_hash is null) or (strategy = 'FINGERPRINT' and target_app_version is null and fingerprint_hash is not null));
|
|
144
|
-
alter table releases add constraint releases_rollout_cohort_count_check
|
|
145
|
-
check (rollout_cohort_count >= 0 and rollout_cohort_count <= 1000);
|
|
146
|
-
alter table releases add constraint releases_operation_check
|
|
147
|
-
check (operation in ('DEPLOY', 'PROMOTE', 'ROLLBACK'));
|
|
148
|
-
alter table release_catalogs add constraint release_catalogs_strategy_target_check
|
|
149
|
-
check ((strategy = 'APP_VERSION' and fingerprint_hash is null) or (strategy = 'FINGERPRINT' and fingerprint_hash is not null));
|
|
150
|
-
alter table release_catalogs add constraint release_catalogs_generation_check
|
|
151
|
-
check (generation >= 1 and generation <= 9007199254740991);
|
|
152
|
-
alter table release_catalogs add constraint release_catalogs_byte_size_check
|
|
153
|
-
check (byte_size >= 0 and byte_size <= 262144);
|
|
154
|
-
alter table bundle_events add constraint bundle_events_type_check
|
|
155
|
-
check (type in ('UPDATE_DOWNLOADED', 'UPDATE_APPLIED', 'RECOVERED', 'UNCHANGED'));
|
|
156
|
-
alter table bundle_events add constraint bundle_events_platform_check
|
|
157
|
-
check (platform in ('ios', 'android'));
|
|
158
|
-
alter table bundle_events add constraint bundle_events_shape_check
|
|
159
|
-
check (((type in ('UPDATE_DOWNLOADED', 'UPDATE_APPLIED', 'RECOVERED')) and from_bundle_id is not null) or (type = 'UNCHANGED' and from_bundle_id is null));
|
|
160
|
-
alter table bundle_events add constraint bundle_events_received_at_check
|
|
161
|
-
check (received_at_ms >= 0);
|
|
162
|
-
alter table api_keys add constraint api_keys_role_check
|
|
163
|
-
check (role = 'client');
|
|
164
|
-
alter table api_keys add constraint api_keys_created_at_check
|
|
165
|
-
check (created_at_ms >= 0);
|
|
166
|
-
alter table api_keys add constraint api_keys_revoked_at_check
|
|
167
|
-
check (revoked_at_ms is null or revoked_at_ms >= 0);
|
|
168
|
-
|
|
169
|
-
alter table bundle_patches add constraint bundle_patches_bundle_id_fk
|
|
170
|
-
foreign key (bundle_id) references bundles(id) on update restrict on delete cascade;
|
|
171
|
-
alter table bundle_patches add constraint bundle_patches_base_bundle_id_fk
|
|
172
|
-
foreign key (base_bundle_id) references bundles(id) on update restrict on delete cascade;
|
|
173
|
-
alter table releases add constraint releases_channel_id_fk
|
|
174
|
-
foreign key (channel_id) references channels(id) on update restrict on delete restrict;
|
|
175
|
-
alter table releases add constraint releases_bundle_id_fk
|
|
176
|
-
foreign key (bundle_id) references bundles(id) on update restrict on delete restrict;
|
|
177
|
-
alter table releases add constraint releases_source_release_id_fk
|
|
178
|
-
foreign key (source_release_id) references releases(id) on update restrict on delete set null;
|
|
179
|
-
alter table release_catalogs add constraint release_catalogs_channel_id_fk
|
|
180
|
-
foreign key (channel_id) references channels(id) on update restrict on delete restrict;
|
|
181
|
-
|
|
182
|
-
insert into private_hot_updater_settings (key, value)
|
|
183
|
-
values ('schema.core', '1.0.0')
|
|
184
|
-
on conflict (key) do update set value = excluded.value;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
CREATE INDEX bundle_events_latest_idx ON bundle_events(install_id, received_at_ms, id);
|
|
188
|
-
CREATE INDEX bundle_event_heads_user_idx ON bundle_event_heads(user_id, install_id);
|
|
189
|
-
CREATE INDEX bundle_event_heads_scope_idx ON bundle_event_heads(platform, channel, received_at_ms);
|
|
190
|
-
CREATE INDEX bundle_event_heads_from_idx ON bundle_event_heads(type, platform, channel, from_bundle_id, received_at_ms);
|
|
191
|
-
CREATE INDEX bundle_event_heads_to_idx ON bundle_event_heads(type, platform, channel, to_bundle_id, received_at_ms);
|
|
3
|
+
CREATE TABLE IF NOT EXISTS "bundles" ("id" varchar(36) COLLATE "C" NOT NULL, "platform" varchar(16) COLLATE "C" NOT NULL, "git_commit_hash" varchar(255) COLLATE "C", "metadata" jsonb NOT NULL, "manifest_storage_uri" text COLLATE "C" NOT NULL, "manifest_file_hash" text COLLATE "C" NOT NULL, "asset_base_storage_uri" text COLLATE "C" NOT NULL, "_refs_bundle_patches_bundle_id" bigint NOT NULL DEFAULT 0, "_refs_bundle_patches_base_bundle_id" bigint NOT NULL DEFAULT 0, "_refs_releases_bundle_id" bigint NOT NULL DEFAULT 0, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
|
|
4
|
+
|
|
5
|
+
CREATE INDEX IF NOT EXISTS "bundles_byPlatform" ON "bundles" ("platform", "id");
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS "bundle_patches" ("id" varchar(255) COLLATE "C" NOT NULL, "bundle_id" varchar(36) COLLATE "C" NOT NULL, "base_bundle_id" varchar(36) COLLATE "C" NOT NULL, "base_file_hash" text COLLATE "C" NOT NULL, "patch_file_hash" text COLLATE "C" NOT NULL, "patch_storage_uri" text COLLATE "C" NOT NULL, "byte_size" bigint NOT NULL, "order_index" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
|
|
8
|
+
|
|
9
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "bundle_patches_pair" ON "bundle_patches" ("bundle_id", "base_bundle_id");
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS "bundle_patches_byBundle" ON "bundle_patches" ("bundle_id", "order_index", "id");
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS "bundle_patches_byBase" ON "bundle_patches" ("base_bundle_id", "bundle_id", "id");
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS "releases" ("id" varchar(36) COLLATE "C" NOT NULL, "revision" bigint NOT NULL, "scope_key" varchar(2048) COLLATE "C" NOT NULL, "channel_id" varchar(1408) COLLATE "C" NOT NULL, "platform" varchar(16) COLLATE "C" NOT NULL, "kind" varchar(16) COLLATE "C" NOT NULL, "bundle_id" varchar(36) COLLATE "C", "strategy" varchar(16) COLLATE "C" NOT NULL, "target_app_version" text COLLATE "C", "fingerprint_hash" varchar(255) COLLATE "C", "enabled" boolean NOT NULL, "should_force_update" boolean NOT NULL, "message" text COLLATE "C", "rollout_cohort_count" bigint NOT NULL, "target_cohorts" jsonb NOT NULL, "operation" varchar(16) COLLATE "C" NOT NULL, "source_release_id" varchar(36) COLLATE "C", "created_at_ms" bigint NOT NULL, "updated_at_ms" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS "releases_byScope" ON "releases" ("scope_key", "id");
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS "releases_byScopeEnabled" ON "releases" ("scope_key", "enabled", "id");
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS "releases_byChannelPlatform" ON "releases" ("channel_id", "platform", "id");
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS "releases_byChannelPlatformEnabled" ON "releases" ("channel_id", "platform", "enabled", "id");
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS "releases_byBundle" ON "releases" ("bundle_id", "id");
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS "release_catalogs" ("scope_key" varchar(2048) COLLATE "C" NOT NULL, "catalog_id" varchar(255) COLLATE "C" NOT NULL, "strategy" varchar(16) COLLATE "C" NOT NULL, "channel_id" varchar(1408) COLLATE "C" NOT NULL, "channel_key" varchar(1400) COLLATE "C" NOT NULL, "platform" varchar(16) COLLATE "C" NOT NULL, "fingerprint_hash" varchar(255) COLLATE "C", "generation" bigint NOT NULL, "payload" text COLLATE "C" NOT NULL, "catalog_hash" varchar(71) COLLATE "C" NOT NULL, "byte_size" bigint NOT NULL, "is_tombstone" boolean NOT NULL, "updated_at_ms" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("scope_key"));
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS "channels" ("id" varchar(1408) COLLATE "C" NOT NULL, "name" varchar(255) COLLATE "C" NOT NULL, "_refs_releases_channel_id" bigint NOT NULL DEFAULT 0, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS "channels_all" ON "channels" ("name", "id");
|
|
32
|
+
|
|
33
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "channels_name" ON "channels" ("name");
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS "bundle_totals" ("platform_key" varchar(16) COLLATE "C" NOT NULL, "_shard" bigint NOT NULL, "bundles" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("platform_key", "_shard"));
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS "bundle_events" ("id" varchar(36) COLLATE "C" NOT NULL, "type" varchar(32) COLLATE "C" NOT NULL, "install_id" varchar(255) COLLATE "C" NOT NULL, "user_id" varchar(255) COLLATE "C", "from_release_id" varchar(36) COLLATE "C", "from_bundle_id" varchar(36) COLLATE "C", "to_release_id" varchar(36) COLLATE "C", "to_bundle_id" varchar(36) COLLATE "C" NOT NULL, "platform" varchar(16) COLLATE "C" NOT NULL, "app_version" text COLLATE "C" NOT NULL, "channel" text COLLATE "C" NOT NULL, "metadata" jsonb NOT NULL, "received_at_ms" bigint NOT NULL, "day" bigint, "movement_install_id" text COLLATE "C", "bundle_ref" jsonb, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS "bundle_events_recent" ON "bundle_events" ("channel", "platform", "day", "received_at_ms", "id");
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS "bundle_events_movementsByInstall" ON "bundle_events" ("movement_install_id", "received_at_ms", "id");
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS "bundle_events__byBundle" ("platform" varchar(16) COLLATE "C" NOT NULL, "channel" text COLLATE "C" NOT NULL, "type" varchar(32) COLLATE "C" NOT NULL, "bundle_ref" text COLLATE "C" NOT NULL, "day" bigint NOT NULL, "received_at_ms" bigint NOT NULL, "id" varchar(36) COLLATE "C" NOT NULL, PRIMARY KEY ("platform", "channel", "type", "bundle_ref", "day", "received_at_ms", "id"));
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS "bundle_events_byDay" ON "bundle_events" ("day", "received_at_ms", "id");
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS "bundle_event_heads" ("install_id" varchar(255) COLLATE "C" NOT NULL, "id" varchar(36) COLLATE "C" NOT NULL, "type" varchar(32) COLLATE "C" NOT NULL, "user_id" varchar(255) COLLATE "C", "from_release_id" varchar(36) COLLATE "C", "from_bundle_id" varchar(36) COLLATE "C", "to_release_id" varchar(36) COLLATE "C", "to_bundle_id" varchar(36) COLLATE "C" NOT NULL, "platform" varchar(16) COLLATE "C" NOT NULL, "app_version" text COLLATE "C" NOT NULL, "channel" text COLLATE "C" NOT NULL, "metadata" jsonb NOT NULL, "received_at_ms" bigint NOT NULL, "current_release_id" varchar(36) COLLATE "C", "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("install_id"));
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS "bundle_event_heads_byUser" ON "bundle_event_heads" ("user_id", "install_id");
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS "insights_overview" ("identity" varchar(32) COLLATE "C" NOT NULL, "bucket_start_ms" bigint NOT NULL, "_shard" bigint NOT NULL, "downloads" bigint NOT NULL, "launches" bigint NOT NULL, "failed_launches" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS "insights_sketches" ("identity" varchar(32) COLLATE "C" NOT NULL, "bucket_start_ms" bigint NOT NULL, "_shard" bigint NOT NULL, "launch_users" text COLLATE "C", "activity_users" text COLLATE "C", "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("identity", "bucket_start_ms", "_shard"));
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS "insights_distribution" ("channel" text COLLATE "C" NOT NULL, "platform" varchar(16) COLLATE "C" NOT NULL, "app_version" text COLLATE "C" NOT NULL, "release_id" varchar(36) COLLATE "C" NOT NULL, "bucket_start_ms" bigint NOT NULL, "_shard" bigint NOT NULL, "latest_installations" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("channel", "platform", "app_version", "release_id", "bucket_start_ms", "_shard"));
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS "insights_distribution_byScope" ON "insights_distribution" ("channel", "platform", "bucket_start_ms", "app_version", "release_id", "_shard");
|
|
58
|
+
|
|
59
|
+
CREATE INDEX IF NOT EXISTS "insights_distribution_byVersion" ON "insights_distribution" ("channel", "platform", "app_version", "bucket_start_ms", "release_id", "_shard");
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS "insights_latest_by_bundle" ("platform" varchar(16) COLLATE "C" NOT NULL, "channel" text COLLATE "C" NOT NULL, "bundle_field" varchar(16) COLLATE "C" NOT NULL, "bundle_id" varchar(36) COLLATE "C" NOT NULL, "type" varchar(32) COLLATE "C" NOT NULL, "bucket_start_ms" bigint NOT NULL, "_shard" bigint NOT NULL, "installations" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("platform", "channel", "bundle_field", "bundle_id", "type", "bucket_start_ms", "_shard"));
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS "insights_outcomes" ("platform" varchar(16) COLLATE "C" NOT NULL, "channel" text COLLATE "C" NOT NULL, "type" varchar(32) COLLATE "C" NOT NULL, "bundle_ref" varchar(41) COLLATE "C" NOT NULL, "bucket_start_ms" bigint NOT NULL, "_shard" bigint NOT NULL, "events" bigint NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("platform", "channel", "type", "bundle_ref", "bucket_start_ms", "_shard"));
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS "api_keys" ("id" varchar(255) COLLATE "C" NOT NULL, "hash" varchar(64) COLLATE "C" NOT NULL, "name" varchar(64) COLLATE "C" NOT NULL, "prefix" varchar(16) COLLATE "C" NOT NULL, "role" varchar(16) COLLATE "C" NOT NULL, "created_at_ms" bigint NOT NULL, "revoked_at_ms" bigint, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS "api_keys_byCreated" ON "api_keys" ("created_at_ms", "id");
|
|
68
|
+
|
|
69
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "api_keys_hash" ON "api_keys" ("hash");
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS "private_hot_updater_settings" ("key" varchar(255) COLLATE "C" NOT NULL, "value" varchar(255) COLLATE "C" NOT NULL, "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("key"));
|
|
72
|
+
|
|
73
|
+
INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.engine', '1', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
|
|
74
|
+
|
|
75
|
+
INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.core', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
|
|
76
|
+
|
|
77
|
+
INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.insights', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
|
|
78
|
+
|
|
79
|
+
INSERT INTO "private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.apiKeys', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
|