@happyvertical/sql 0.89.1 → 0.89.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/chunks/{connection-cache-WEH2ulk5.js → connection-cache-Da5UcY5u.js} +140 -9
- package/dist/chunks/{connection-cache-WEH2ulk5.js.map → connection-cache-Da5UcY5u.js.map} +1 -1
- package/dist/chunks/{duckdb-Di0KdU5i.js → duckdb-Ba6QVk6i.js} +2 -2
- package/dist/chunks/{duckdb-Di0KdU5i.js.map → duckdb-Ba6QVk6i.js.map} +1 -1
- package/dist/chunks/{sqlite-BTmP_O5Q.js → sqlite-BPI6YDpW.js} +4 -4
- package/dist/chunks/{sqlite-BTmP_O5Q.js.map → sqlite-BPI6YDpW.js.map} +1 -1
- package/dist/chunks/{sqlite-connection-cache-DfB52hp6.js → sqlite-connection-cache-DwlyWaqG.js} +2 -2
- package/dist/chunks/{sqlite-connection-cache-DfB52hp6.js.map → sqlite-connection-cache-DwlyWaqG.js.map} +1 -1
- package/dist/chunks/{sqlite-native-FrjMCnBh.js → sqlite-native-DBbBMvTD.js} +3 -3
- package/dist/chunks/{sqlite-native-FrjMCnBh.js.map → sqlite-native-DBbBMvTD.js.map} +1 -1
- package/dist/chunks/types-uVFHgaSU.js.map +1 -1
- package/dist/index.js +193 -23
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/shared/alter-utils.d.ts +21 -5
- package/dist/shared/alter-utils.d.ts.map +1 -1
- package/dist/shared/connection-cache.d.ts +13 -1
- package/dist/shared/connection-cache.d.ts.map +1 -1
- package/dist/shared/types.d.ts +6 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -83,6 +83,20 @@ cached adapter. Combine it with `cache: false` to evict first and return an
|
|
|
83
83
|
uncached replacement. A concurrent initializer caught by eviction is closed
|
|
84
84
|
and cannot repopulate the cache.
|
|
85
85
|
|
|
86
|
+
With automatic file registration enabled (the default), cached JSON adapters
|
|
87
|
+
also detect external changes to JSON data and companion `.schema.sql` files when
|
|
88
|
+
a later `getDatabase()` call acquires the same cache identity. A changed, added,
|
|
89
|
+
or removed source file causes the old DuckDB adapter to close before one fresh
|
|
90
|
+
shared adapter is created. Existing references point to the closed adapter
|
|
91
|
+
after replacement; acquire the database again before continuing work after an
|
|
92
|
+
external file change. Writes made through the adapter refresh its snapshot and
|
|
93
|
+
do not invalidate the adapter itself when the tracked sources are otherwise
|
|
94
|
+
unchanged. If an external change is already visible before an adapter export,
|
|
95
|
+
the export fails closed so the caller can reacquire the database and retry.
|
|
96
|
+
Freshness checks are not a cross-process file lock: an external process writing
|
|
97
|
+
the same table's JSON or companion schema file at the same instant as an adapter
|
|
98
|
+
export remains last-writer-wins and must be coordinated by the application.
|
|
99
|
+
|
|
86
100
|
The JSON adapter's exported `clearConnectionCache()` helper is asynchronous:
|
|
87
101
|
always `await clearConnectionCache()` before opening a replacement connection.
|
|
88
102
|
|
|
@@ -248,6 +262,13 @@ object with a `toString` — is rejected outright rather than coerced, so the va
|
|
|
248
262
|
validated is always the value interpolated. Values are always parameterized and
|
|
249
263
|
are unaffected.
|
|
250
264
|
|
|
265
|
+
PostgreSQL `upsert()` additionally lowercases each validated column name before
|
|
266
|
+
double-quoting it. PostgreSQL has always folded the adapter's unquoted column
|
|
267
|
+
names to lowercase, so this preserves existing behavior (`mixedCase` still
|
|
268
|
+
addresses `mixedcase`) while allowing reserved words such as `end` in insert,
|
|
269
|
+
conflict and update positions. Delimited, case-sensitive physical names such as
|
|
270
|
+
`"MixedCase"` remain outside the CRUD identifier contract.
|
|
271
|
+
|
|
251
272
|
`buildWhere` separates a supported operator suffix (`'price >'`,
|
|
252
273
|
`'name contains'`) from the field and then validates the field as an identifier.
|
|
253
274
|
Unsupported suffixes remain part of the field and fail validation rather than
|
|
@@ -911,11 +911,12 @@ function validateColumnName(columnName) {
|
|
|
911
911
|
* Validates every identifier in a column list to prevent SQL injection
|
|
912
912
|
*
|
|
913
913
|
* Column names reaching the CRUD methods come from `Object.keys(data)` and the
|
|
914
|
-
* caller's `conflictColumns`, both of which are interpolated into SQL.
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
914
|
+
* caller's `conflictColumns`, both of which are interpolated into SQL. The
|
|
915
|
+
* accepted shape has no character that can end an identifier. Adapters may
|
|
916
|
+
* render a validated name bare, or quote it under an adapter-specific contract.
|
|
917
|
+
* PostgreSQL upsert uses {@link quotePostgresColumnName}, which canonicalizes
|
|
918
|
+
* the name before quoting so its established unquoted case-folding semantics
|
|
919
|
+
* do not change.
|
|
919
920
|
*
|
|
920
921
|
* @param columnNames - Column names to validate
|
|
921
922
|
* @throws Error if any column name contains invalid characters
|
|
@@ -924,6 +925,24 @@ function validateColumnNames(columnNames) {
|
|
|
924
925
|
for (const columnName of columnNames) validateColumnName(columnName);
|
|
925
926
|
}
|
|
926
927
|
/**
|
|
928
|
+
* Quotes a validated column name using PostgreSQL's historic case semantics.
|
|
929
|
+
*
|
|
930
|
+
* PostgreSQL folds every unquoted identifier to lowercase. The CRUD API has
|
|
931
|
+
* always accepted mixed-case inputs while rendering them unquoted, so quoting
|
|
932
|
+
* the caller's spelling directly would silently change which physical column
|
|
933
|
+
* is addressed. Validate the existing plain-identifier contract first, then
|
|
934
|
+
* lowercase before quoting: `mixedCase` still addresses `mixedcase`, while a
|
|
935
|
+
* reserved word such as `end` is no longer parsed as SQL syntax.
|
|
936
|
+
*
|
|
937
|
+
* @param columnName - Plain column name to render for PostgreSQL
|
|
938
|
+
* @returns A lowercase, double-quoted PostgreSQL identifier
|
|
939
|
+
* @throws Error if the column name is outside the CRUD identifier contract
|
|
940
|
+
*/
|
|
941
|
+
function quotePostgresColumnName(columnName) {
|
|
942
|
+
validateColumnName(columnName);
|
|
943
|
+
return `"${columnName.toLowerCase()}"`;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
927
946
|
* Escapes an identifier for interpolation inside double quotes
|
|
928
947
|
*
|
|
929
948
|
* For the positions that already quote the identifier — the DuckDB and JSON
|
|
@@ -976,15 +995,17 @@ function validateIndexName(indexName) {
|
|
|
976
995
|
}
|
|
977
996
|
//#endregion
|
|
978
997
|
//#region src/shared/connection-cache.ts
|
|
998
|
+
var CACHED_VALIDATION_CAPABILITY = Symbol.for("@happyvertical/sql/connection-cache-cached-validation/v1");
|
|
979
999
|
/** Race-safe async resource cache used by database adapters. */
|
|
980
1000
|
var ConnectionCache = class {
|
|
1001
|
+
[CACHED_VALIDATION_CAPABILITY] = true;
|
|
981
1002
|
#connections = /* @__PURE__ */ new Map();
|
|
982
1003
|
#pending = /* @__PURE__ */ new Map();
|
|
983
1004
|
#evictions = /* @__PURE__ */ new Map();
|
|
984
1005
|
#requests = /* @__PURE__ */ new Map();
|
|
985
1006
|
#clearing = /* @__PURE__ */ new Map();
|
|
986
1007
|
#failedClosures = /* @__PURE__ */ new Map();
|
|
987
|
-
async getOrCreate(key, options, create, close) {
|
|
1008
|
+
async getOrCreate(key, options, create, close, validateCached) {
|
|
988
1009
|
validateDatabaseCacheOptions(options);
|
|
989
1010
|
const request = key && options.cache !== false ? this.#registerRequest(key) : void 0;
|
|
990
1011
|
try {
|
|
@@ -1002,7 +1023,17 @@ var ConnectionCache = class {
|
|
|
1002
1023
|
}
|
|
1003
1024
|
this.#throwIfFailedClosure(key);
|
|
1004
1025
|
const cached = this.#connections.get(key);
|
|
1005
|
-
if (cached)
|
|
1026
|
+
if (cached !== void 0) {
|
|
1027
|
+
if (!validateCached) return cached;
|
|
1028
|
+
const isValid = await validateCached(cached);
|
|
1029
|
+
this.#throwIfCancelled(request);
|
|
1030
|
+
if (isValid && this.#connections.get(key) === cached && !this.#evictions.has(key)) return cached;
|
|
1031
|
+
if (!isValid && this.#connections.get(key) === cached && !this.#evictions.has(key)) {
|
|
1032
|
+
await this.evict(key, close);
|
|
1033
|
+
this.#throwIfCancelled(request);
|
|
1034
|
+
}
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1006
1037
|
let pending = this.#pending.get(key);
|
|
1007
1038
|
if (!pending) {
|
|
1008
1039
|
this.#throwIfCancelled(request);
|
|
@@ -1165,10 +1196,110 @@ var ConnectionCache = class {
|
|
|
1165
1196
|
}
|
|
1166
1197
|
}
|
|
1167
1198
|
};
|
|
1199
|
+
/**
|
|
1200
|
+
* Adds cached-resource validation to a shared cache created by an older module.
|
|
1201
|
+
*
|
|
1202
|
+
* JSON connection caches live on `globalThis`, so a compatible older SDK copy
|
|
1203
|
+
* can create the shared object before this module loads. Mutating that object
|
|
1204
|
+
* preserves one cache for both module copies while upgrading its four-argument
|
|
1205
|
+
* `getOrCreate` method to the current validation contract.
|
|
1206
|
+
*/
|
|
1207
|
+
function enableCachedValidation(cache) {
|
|
1208
|
+
if (cache[CACHED_VALIDATION_CAPABILITY]) return cache;
|
|
1209
|
+
const legacyGetOrCreate = cache.getOrCreate.bind(cache);
|
|
1210
|
+
const legacyClear = cache.clear.bind(cache);
|
|
1211
|
+
const locks = /* @__PURE__ */ new Map();
|
|
1212
|
+
let clearGeneration = 0;
|
|
1213
|
+
let activeClear;
|
|
1214
|
+
const runLocked = async (key, work) => {
|
|
1215
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
1216
|
+
let release;
|
|
1217
|
+
const current = new Promise((resolve) => {
|
|
1218
|
+
release = resolve;
|
|
1219
|
+
});
|
|
1220
|
+
const chain = previous.then(() => current, () => current);
|
|
1221
|
+
locks.set(key, chain);
|
|
1222
|
+
await previous.catch(() => {});
|
|
1223
|
+
try {
|
|
1224
|
+
return await work();
|
|
1225
|
+
} finally {
|
|
1226
|
+
release();
|
|
1227
|
+
if (locks.get(key) === chain) locks.delete(key);
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
Object.defineProperty(cache, "getOrCreate", {
|
|
1231
|
+
configurable: true,
|
|
1232
|
+
value: async (key, options, create, close, validateCached) => {
|
|
1233
|
+
if (!key || options.cache === false) return legacyGetOrCreate(key, options, create, close);
|
|
1234
|
+
const requestGeneration = clearGeneration;
|
|
1235
|
+
const throwIfCleared = () => {
|
|
1236
|
+
if (activeClear || requestGeneration !== clearGeneration) throw new Error("Database connection initialization was cancelled by cache clear");
|
|
1237
|
+
};
|
|
1238
|
+
return runLocked(key, async () => {
|
|
1239
|
+
throwIfCleared();
|
|
1240
|
+
let requestOptions = options;
|
|
1241
|
+
while (true) {
|
|
1242
|
+
throwIfCleared();
|
|
1243
|
+
const candidate = await legacyGetOrCreate(key, requestOptions, create, close);
|
|
1244
|
+
throwIfCleared();
|
|
1245
|
+
if (!validateCached) return candidate;
|
|
1246
|
+
const isValid = await validateCached(candidate);
|
|
1247
|
+
throwIfCleared();
|
|
1248
|
+
if (isValid) {
|
|
1249
|
+
const confirmed = await legacyGetOrCreate(key, {
|
|
1250
|
+
...requestOptions,
|
|
1251
|
+
clearCache: false
|
|
1252
|
+
}, create, close);
|
|
1253
|
+
throwIfCleared();
|
|
1254
|
+
if (confirmed === candidate) return candidate;
|
|
1255
|
+
} else await cache.evict(key, close);
|
|
1256
|
+
requestOptions = {
|
|
1257
|
+
...options,
|
|
1258
|
+
clearCache: false
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
},
|
|
1263
|
+
writable: true
|
|
1264
|
+
});
|
|
1265
|
+
Object.defineProperty(cache, "clear", {
|
|
1266
|
+
configurable: true,
|
|
1267
|
+
value: async (close) => {
|
|
1268
|
+
if (activeClear) return activeClear;
|
|
1269
|
+
clearGeneration += 1;
|
|
1270
|
+
const clearing = (async () => {
|
|
1271
|
+
let clearError;
|
|
1272
|
+
let clearFailed = false;
|
|
1273
|
+
try {
|
|
1274
|
+
await legacyClear(close);
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
clearFailed = true;
|
|
1277
|
+
clearError = error;
|
|
1278
|
+
}
|
|
1279
|
+
while (locks.size > 0) await Promise.allSettled([...locks.values()]);
|
|
1280
|
+
if (clearFailed) throw clearError;
|
|
1281
|
+
})();
|
|
1282
|
+
activeClear = clearing;
|
|
1283
|
+
try {
|
|
1284
|
+
await clearing;
|
|
1285
|
+
} finally {
|
|
1286
|
+
if (activeClear === clearing) activeClear = void 0;
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
writable: true
|
|
1290
|
+
});
|
|
1291
|
+
Object.defineProperty(cache, CACHED_VALIDATION_CAPABILITY, {
|
|
1292
|
+
configurable: false,
|
|
1293
|
+
enumerable: false,
|
|
1294
|
+
value: true,
|
|
1295
|
+
writable: false
|
|
1296
|
+
});
|
|
1297
|
+
return cache;
|
|
1298
|
+
}
|
|
1168
1299
|
function validateDatabaseCacheOptions(options) {
|
|
1169
1300
|
if (options.dbid === "") throw new Error("Database cache dbid must be a non-empty string");
|
|
1170
1301
|
}
|
|
1171
1302
|
//#endregion
|
|
1172
|
-
export {
|
|
1303
|
+
export { raw as _, escapeStringLiteral as a, wrapDatabaseError as b, quotePostgresColumnName as c, validateIndexName as d, validateTableName as f, parseConditionKey as g, formatDbError as h, escapeQuotedIdentifier as i, validateColumnName as l, buildWhere as m, enableCachedValidation as n, generateAddColumnStatement as o, DatabaseSchemaManager as p, validateDatabaseCacheOptions as r, generateCreateIndexStatement as s, ConnectionCache as t, validateColumnNames as u, resolveInsertColumns as v, unwrapRawKey as y };
|
|
1173
1304
|
|
|
1174
|
-
//# sourceMappingURL=connection-cache-
|
|
1305
|
+
//# sourceMappingURL=connection-cache-Da5UcY5u.js.map
|