latticesql 4.3.2 → 4.3.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/dist/cli.js CHANGED
@@ -1704,7 +1704,7 @@ async function registerPostgresPolyfills(run) {
1704
1704
  await run(sql);
1705
1705
  } catch (err) {
1706
1706
  const msg = err instanceof Error ? err.message : String(err);
1707
- if (/permission denied/i.test(msg)) {
1707
+ if (/permission denied/i.test(msg) || /must be owner of/i.test(msg)) {
1708
1708
  permissionDenied = true;
1709
1709
  } else {
1710
1710
  console.warn(`[PostgresAdapter] ${warn}`, msg);
@@ -2222,40 +2222,111 @@ var init_postgres = __esm({
2222
2222
  SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
2223
2223
  $fn$;
2224
2224
  END IF;
2225
+ END $do$;`
2226
+ },
2227
+ {
2228
+ warn: "could not register strftime format helper:",
2229
+ // Shared format translator (SQLite strftime tokens → to_char patterns), in UTC.
2230
+ // Factored out so the 2-arg and 3-arg strftime overloads share one definition.
2231
+ // Each polyfill below is CREATE OR REPLACE (so an existing cloud's prior copy is
2232
+ // UPGRADED when the owner opens), wrapped in a DO block whose EXCEPTION swallows a
2233
+ // scoped member's insufficient-privilege failure IN A SUBTRANSACTION — so a member
2234
+ // that runs registration inside a transaction is NOT aborted. ("must be owner of
2235
+ // function", raised when a non-owner CREATE OR REPLACEs another role's function, is
2236
+ // SQLSTATE 42501 / `insufficient_privilege`, same class as "permission denied".) The
2237
+ // owner already owns + replaced the function; the member simply uses the owner's
2238
+ // copy — and `ownPolyfillsByGroup` re-owns the whole set to the member group so any
2239
+ // member can replace them on a later upgrade.
2240
+ sql: `DO $do$ BEGIN
2241
+ CREATE OR REPLACE FUNCTION __lattice_strftime_fmt(ts timestamptz, format text)
2242
+ RETURNS text
2243
+ LANGUAGE sql
2244
+ IMMUTABLE
2245
+ AS $fn$
2246
+ SELECT to_char(
2247
+ ts AT TIME ZONE 'UTC',
2248
+ replace(replace(replace(replace(replace(replace(replace(replace(
2249
+ format,
2250
+ '%Y', 'YYYY'),
2251
+ '%m', 'MM'),
2252
+ '%d', 'DD'),
2253
+ '%H', 'HH24'),
2254
+ '%M', 'MI'),
2255
+ '%S', 'SS'),
2256
+ '%f', 'MS'),
2257
+ 'T', '"T"')
2258
+ );
2259
+ $fn$;
2260
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
2225
2261
  END $do$;`
2226
2262
  },
2227
2263
  {
2228
2264
  warn: "could not register strftime polyfill:",
2265
+ // The pre-4.3.3 strftime cast the modifier straight to timestamptz and threw
2266
+ // `invalid input syntax for type timestamp with time zone: ""` on a legacy ''
2267
+ // value (3.x stored nullable TEXT timestamps as ''), bricking the whole workspace
2268
+ // open. Now an empty, whitespace, or unparseable time string returns NULL (SQLite's
2269
+ // strftime semantics) instead of aborting the query.
2229
2270
  sql: `DO $do$ BEGIN
2230
- IF to_regprocedure('strftime(text, text)') IS NULL THEN
2231
- CREATE FUNCTION strftime(format text, modifier text)
2232
- RETURNS text
2233
- LANGUAGE plpgsql
2234
- IMMUTABLE
2235
- AS $fn$
2236
- DECLARE ts timestamptz;
2237
- BEGIN
2238
- IF modifier = 'now' THEN
2239
- ts := now();
2240
- ELSE
2271
+ CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
2272
+ RETURNS text
2273
+ LANGUAGE plpgsql
2274
+ IMMUTABLE
2275
+ AS $fn$
2276
+ DECLARE ts timestamptz;
2277
+ BEGIN
2278
+ IF modifier = 'now' THEN
2279
+ ts := now();
2280
+ ELSIF NULLIF(btrim(modifier), '') IS NULL THEN
2281
+ RETURN NULL;
2282
+ ELSE
2283
+ BEGIN
2241
2284
  ts := modifier::timestamptz;
2242
- END IF;
2243
- RETURN to_char(
2244
- ts AT TIME ZONE 'UTC',
2245
- replace(replace(replace(replace(replace(replace(replace(replace(
2246
- format,
2247
- '%Y', 'YYYY'),
2248
- '%m', 'MM'),
2249
- '%d', 'DD'),
2250
- '%H', 'HH24'),
2251
- '%M', 'MI'),
2252
- '%S', 'SS'),
2253
- '%f', 'MS'),
2254
- 'T', '"T"')
2255
- );
2285
+ EXCEPTION WHEN others THEN
2286
+ RETURN NULL;
2287
+ END;
2288
+ END IF;
2289
+ RETURN __lattice_strftime_fmt(ts, format);
2290
+ END;
2291
+ $fn$;
2292
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
2293
+ END $do$;`
2294
+ },
2295
+ {
2296
+ warn: "could not register 3-arg strftime polyfill:",
2297
+ // SQLite's 3-arg strftime(format, timestring, modifier) — used by the changelog
2298
+ // retention prune (e.g. strftime('%Y-...','now','-30 days')). Postgres had no
2299
+ // 3-arg overload, so that prune threw `function strftime(...) does not exist` on
2300
+ // every PG cloud with retention. Resolve the base time, apply the modifier as an
2301
+ // interval, then reuse the shared formatter. Empty/invalid → NULL, never throws.
2302
+ sql: `DO $do$ BEGIN
2303
+ CREATE OR REPLACE FUNCTION strftime(format text, timestring text, modifier text)
2304
+ RETURNS text
2305
+ LANGUAGE plpgsql
2306
+ IMMUTABLE
2307
+ AS $fn$
2308
+ DECLARE ts timestamptz;
2309
+ BEGIN
2310
+ IF timestring = 'now' THEN
2311
+ ts := now();
2312
+ ELSIF NULLIF(btrim(timestring), '') IS NULL THEN
2313
+ RETURN NULL;
2314
+ ELSE
2315
+ BEGIN
2316
+ ts := timestring::timestamptz;
2317
+ EXCEPTION WHEN others THEN
2318
+ RETURN NULL;
2319
+ END;
2320
+ END IF;
2321
+ BEGIN
2322
+ ts := ts + modifier::interval;
2323
+ EXCEPTION WHEN others THEN
2324
+ RETURN NULL;
2256
2325
  END;
2257
- $fn$;
2258
- END IF;
2326
+ RETURN __lattice_strftime_fmt(ts, format);
2327
+ END;
2328
+ $fn$;
2329
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
2259
2330
  END $do$;`
2260
2331
  }
2261
2332
  ];
@@ -9180,7 +9251,12 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
9180
9251
  async function ownPolyfillsByGroup(db) {
9181
9252
  if (!isPg(db)) return;
9182
9253
  const group3 = await memberGroupFor(db);
9183
- for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
9254
+ for (const sig of [
9255
+ "json_extract(text, text)",
9256
+ "strftime(text, text)",
9257
+ "strftime(text, text, text)",
9258
+ "__lattice_strftime_fmt(timestamptz, text)"
9259
+ ]) {
9184
9260
  try {
9185
9261
  const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
9186
9262
  if (reg?.reg == null) continue;
@@ -82020,7 +82096,7 @@ function printHelp() {
82020
82096
  );
82021
82097
  }
82022
82098
  function getVersion() {
82023
- if (true) return "4.3.2";
82099
+ if (true) return "4.3.3";
82024
82100
  try {
82025
82101
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
82026
82102
  const pkg = JSON.parse(readFileSync26(pkgPath, "utf-8"));
@@ -835,7 +835,12 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
835
835
  async function ownPolyfillsByGroup(db) {
836
836
  if (!isPg(db)) return;
837
837
  const group3 = await memberGroupFor(db);
838
- for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
838
+ for (const sig of [
839
+ "json_extract(text, text)",
840
+ "strftime(text, text)",
841
+ "strftime(text, text, text)",
842
+ "__lattice_strftime_fmt(timestamptz, text)"
843
+ ]) {
839
844
  try {
840
845
  const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
841
846
  if (reg?.reg == null) continue;
@@ -1781,7 +1786,7 @@ async function registerPostgresPolyfills(run) {
1781
1786
  await run(sql);
1782
1787
  } catch (err) {
1783
1788
  const msg = err instanceof Error ? err.message : String(err);
1784
- if (/permission denied/i.test(msg)) {
1789
+ if (/permission denied/i.test(msg) || /must be owner of/i.test(msg)) {
1785
1790
  permissionDenied = true;
1786
1791
  } else {
1787
1792
  console.warn(`[PostgresAdapter] ${warn}`, msg);
@@ -2299,40 +2304,111 @@ var init_postgres = __esm({
2299
2304
  SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
2300
2305
  $fn$;
2301
2306
  END IF;
2307
+ END $do$;`
2308
+ },
2309
+ {
2310
+ warn: "could not register strftime format helper:",
2311
+ // Shared format translator (SQLite strftime tokens → to_char patterns), in UTC.
2312
+ // Factored out so the 2-arg and 3-arg strftime overloads share one definition.
2313
+ // Each polyfill below is CREATE OR REPLACE (so an existing cloud's prior copy is
2314
+ // UPGRADED when the owner opens), wrapped in a DO block whose EXCEPTION swallows a
2315
+ // scoped member's insufficient-privilege failure IN A SUBTRANSACTION — so a member
2316
+ // that runs registration inside a transaction is NOT aborted. ("must be owner of
2317
+ // function", raised when a non-owner CREATE OR REPLACEs another role's function, is
2318
+ // SQLSTATE 42501 / `insufficient_privilege`, same class as "permission denied".) The
2319
+ // owner already owns + replaced the function; the member simply uses the owner's
2320
+ // copy — and `ownPolyfillsByGroup` re-owns the whole set to the member group so any
2321
+ // member can replace them on a later upgrade.
2322
+ sql: `DO $do$ BEGIN
2323
+ CREATE OR REPLACE FUNCTION __lattice_strftime_fmt(ts timestamptz, format text)
2324
+ RETURNS text
2325
+ LANGUAGE sql
2326
+ IMMUTABLE
2327
+ AS $fn$
2328
+ SELECT to_char(
2329
+ ts AT TIME ZONE 'UTC',
2330
+ replace(replace(replace(replace(replace(replace(replace(replace(
2331
+ format,
2332
+ '%Y', 'YYYY'),
2333
+ '%m', 'MM'),
2334
+ '%d', 'DD'),
2335
+ '%H', 'HH24'),
2336
+ '%M', 'MI'),
2337
+ '%S', 'SS'),
2338
+ '%f', 'MS'),
2339
+ 'T', '"T"')
2340
+ );
2341
+ $fn$;
2342
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
2302
2343
  END $do$;`
2303
2344
  },
2304
2345
  {
2305
2346
  warn: "could not register strftime polyfill:",
2347
+ // The pre-4.3.3 strftime cast the modifier straight to timestamptz and threw
2348
+ // `invalid input syntax for type timestamp with time zone: ""` on a legacy ''
2349
+ // value (3.x stored nullable TEXT timestamps as ''), bricking the whole workspace
2350
+ // open. Now an empty, whitespace, or unparseable time string returns NULL (SQLite's
2351
+ // strftime semantics) instead of aborting the query.
2306
2352
  sql: `DO $do$ BEGIN
2307
- IF to_regprocedure('strftime(text, text)') IS NULL THEN
2308
- CREATE FUNCTION strftime(format text, modifier text)
2309
- RETURNS text
2310
- LANGUAGE plpgsql
2311
- IMMUTABLE
2312
- AS $fn$
2313
- DECLARE ts timestamptz;
2314
- BEGIN
2315
- IF modifier = 'now' THEN
2316
- ts := now();
2317
- ELSE
2353
+ CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
2354
+ RETURNS text
2355
+ LANGUAGE plpgsql
2356
+ IMMUTABLE
2357
+ AS $fn$
2358
+ DECLARE ts timestamptz;
2359
+ BEGIN
2360
+ IF modifier = 'now' THEN
2361
+ ts := now();
2362
+ ELSIF NULLIF(btrim(modifier), '') IS NULL THEN
2363
+ RETURN NULL;
2364
+ ELSE
2365
+ BEGIN
2318
2366
  ts := modifier::timestamptz;
2319
- END IF;
2320
- RETURN to_char(
2321
- ts AT TIME ZONE 'UTC',
2322
- replace(replace(replace(replace(replace(replace(replace(replace(
2323
- format,
2324
- '%Y', 'YYYY'),
2325
- '%m', 'MM'),
2326
- '%d', 'DD'),
2327
- '%H', 'HH24'),
2328
- '%M', 'MI'),
2329
- '%S', 'SS'),
2330
- '%f', 'MS'),
2331
- 'T', '"T"')
2332
- );
2367
+ EXCEPTION WHEN others THEN
2368
+ RETURN NULL;
2369
+ END;
2370
+ END IF;
2371
+ RETURN __lattice_strftime_fmt(ts, format);
2372
+ END;
2373
+ $fn$;
2374
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
2375
+ END $do$;`
2376
+ },
2377
+ {
2378
+ warn: "could not register 3-arg strftime polyfill:",
2379
+ // SQLite's 3-arg strftime(format, timestring, modifier) — used by the changelog
2380
+ // retention prune (e.g. strftime('%Y-...','now','-30 days')). Postgres had no
2381
+ // 3-arg overload, so that prune threw `function strftime(...) does not exist` on
2382
+ // every PG cloud with retention. Resolve the base time, apply the modifier as an
2383
+ // interval, then reuse the shared formatter. Empty/invalid → NULL, never throws.
2384
+ sql: `DO $do$ BEGIN
2385
+ CREATE OR REPLACE FUNCTION strftime(format text, timestring text, modifier text)
2386
+ RETURNS text
2387
+ LANGUAGE plpgsql
2388
+ IMMUTABLE
2389
+ AS $fn$
2390
+ DECLARE ts timestamptz;
2391
+ BEGIN
2392
+ IF timestring = 'now' THEN
2393
+ ts := now();
2394
+ ELSIF NULLIF(btrim(timestring), '') IS NULL THEN
2395
+ RETURN NULL;
2396
+ ELSE
2397
+ BEGIN
2398
+ ts := timestring::timestamptz;
2399
+ EXCEPTION WHEN others THEN
2400
+ RETURN NULL;
2401
+ END;
2402
+ END IF;
2403
+ BEGIN
2404
+ ts := ts + modifier::interval;
2405
+ EXCEPTION WHEN others THEN
2406
+ RETURN NULL;
2333
2407
  END;
2334
- $fn$;
2335
- END IF;
2408
+ RETURN __lattice_strftime_fmt(ts, format);
2409
+ END;
2410
+ $fn$;
2411
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
2336
2412
  END $do$;`
2337
2413
  }
2338
2414
  ];
@@ -81598,7 +81674,7 @@ ${e6.stack ?? ""}`
81598
81674
  }
81599
81675
 
81600
81676
  // src/desktop-entry.ts
81601
- var VERSION2 = true ? "4.3.2" : "unknown";
81677
+ var VERSION2 = true ? "4.3.3" : "unknown";
81602
81678
  export {
81603
81679
  VERSION2 as VERSION,
81604
81680
  ensureRootForGui,
package/dist/index.cjs CHANGED
@@ -917,7 +917,7 @@ async function registerPostgresPolyfills(run) {
917
917
  await run(sql);
918
918
  } catch (err) {
919
919
  const msg = err instanceof Error ? err.message : String(err);
920
- if (/permission denied/i.test(msg)) {
920
+ if (/permission denied/i.test(msg) || /must be owner of/i.test(msg)) {
921
921
  permissionDenied = true;
922
922
  } else {
923
923
  console.warn(`[PostgresAdapter] ${warn}`, msg);
@@ -1439,40 +1439,111 @@ var init_postgres = __esm({
1439
1439
  SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
1440
1440
  $fn$;
1441
1441
  END IF;
1442
+ END $do$;`
1443
+ },
1444
+ {
1445
+ warn: "could not register strftime format helper:",
1446
+ // Shared format translator (SQLite strftime tokens → to_char patterns), in UTC.
1447
+ // Factored out so the 2-arg and 3-arg strftime overloads share one definition.
1448
+ // Each polyfill below is CREATE OR REPLACE (so an existing cloud's prior copy is
1449
+ // UPGRADED when the owner opens), wrapped in a DO block whose EXCEPTION swallows a
1450
+ // scoped member's insufficient-privilege failure IN A SUBTRANSACTION — so a member
1451
+ // that runs registration inside a transaction is NOT aborted. ("must be owner of
1452
+ // function", raised when a non-owner CREATE OR REPLACEs another role's function, is
1453
+ // SQLSTATE 42501 / `insufficient_privilege`, same class as "permission denied".) The
1454
+ // owner already owns + replaced the function; the member simply uses the owner's
1455
+ // copy — and `ownPolyfillsByGroup` re-owns the whole set to the member group so any
1456
+ // member can replace them on a later upgrade.
1457
+ sql: `DO $do$ BEGIN
1458
+ CREATE OR REPLACE FUNCTION __lattice_strftime_fmt(ts timestamptz, format text)
1459
+ RETURNS text
1460
+ LANGUAGE sql
1461
+ IMMUTABLE
1462
+ AS $fn$
1463
+ SELECT to_char(
1464
+ ts AT TIME ZONE 'UTC',
1465
+ replace(replace(replace(replace(replace(replace(replace(replace(
1466
+ format,
1467
+ '%Y', 'YYYY'),
1468
+ '%m', 'MM'),
1469
+ '%d', 'DD'),
1470
+ '%H', 'HH24'),
1471
+ '%M', 'MI'),
1472
+ '%S', 'SS'),
1473
+ '%f', 'MS'),
1474
+ 'T', '"T"')
1475
+ );
1476
+ $fn$;
1477
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1442
1478
  END $do$;`
1443
1479
  },
1444
1480
  {
1445
1481
  warn: "could not register strftime polyfill:",
1482
+ // The pre-4.3.3 strftime cast the modifier straight to timestamptz and threw
1483
+ // `invalid input syntax for type timestamp with time zone: ""` on a legacy ''
1484
+ // value (3.x stored nullable TEXT timestamps as ''), bricking the whole workspace
1485
+ // open. Now an empty, whitespace, or unparseable time string returns NULL (SQLite's
1486
+ // strftime semantics) instead of aborting the query.
1446
1487
  sql: `DO $do$ BEGIN
1447
- IF to_regprocedure('strftime(text, text)') IS NULL THEN
1448
- CREATE FUNCTION strftime(format text, modifier text)
1449
- RETURNS text
1450
- LANGUAGE plpgsql
1451
- IMMUTABLE
1452
- AS $fn$
1453
- DECLARE ts timestamptz;
1454
- BEGIN
1455
- IF modifier = 'now' THEN
1456
- ts := now();
1457
- ELSE
1488
+ CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
1489
+ RETURNS text
1490
+ LANGUAGE plpgsql
1491
+ IMMUTABLE
1492
+ AS $fn$
1493
+ DECLARE ts timestamptz;
1494
+ BEGIN
1495
+ IF modifier = 'now' THEN
1496
+ ts := now();
1497
+ ELSIF NULLIF(btrim(modifier), '') IS NULL THEN
1498
+ RETURN NULL;
1499
+ ELSE
1500
+ BEGIN
1458
1501
  ts := modifier::timestamptz;
1459
- END IF;
1460
- RETURN to_char(
1461
- ts AT TIME ZONE 'UTC',
1462
- replace(replace(replace(replace(replace(replace(replace(replace(
1463
- format,
1464
- '%Y', 'YYYY'),
1465
- '%m', 'MM'),
1466
- '%d', 'DD'),
1467
- '%H', 'HH24'),
1468
- '%M', 'MI'),
1469
- '%S', 'SS'),
1470
- '%f', 'MS'),
1471
- 'T', '"T"')
1472
- );
1502
+ EXCEPTION WHEN others THEN
1503
+ RETURN NULL;
1504
+ END;
1505
+ END IF;
1506
+ RETURN __lattice_strftime_fmt(ts, format);
1507
+ END;
1508
+ $fn$;
1509
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1510
+ END $do$;`
1511
+ },
1512
+ {
1513
+ warn: "could not register 3-arg strftime polyfill:",
1514
+ // SQLite's 3-arg strftime(format, timestring, modifier) — used by the changelog
1515
+ // retention prune (e.g. strftime('%Y-...','now','-30 days')). Postgres had no
1516
+ // 3-arg overload, so that prune threw `function strftime(...) does not exist` on
1517
+ // every PG cloud with retention. Resolve the base time, apply the modifier as an
1518
+ // interval, then reuse the shared formatter. Empty/invalid → NULL, never throws.
1519
+ sql: `DO $do$ BEGIN
1520
+ CREATE OR REPLACE FUNCTION strftime(format text, timestring text, modifier text)
1521
+ RETURNS text
1522
+ LANGUAGE plpgsql
1523
+ IMMUTABLE
1524
+ AS $fn$
1525
+ DECLARE ts timestamptz;
1526
+ BEGIN
1527
+ IF timestring = 'now' THEN
1528
+ ts := now();
1529
+ ELSIF NULLIF(btrim(timestring), '') IS NULL THEN
1530
+ RETURN NULL;
1531
+ ELSE
1532
+ BEGIN
1533
+ ts := timestring::timestamptz;
1534
+ EXCEPTION WHEN others THEN
1535
+ RETURN NULL;
1536
+ END;
1537
+ END IF;
1538
+ BEGIN
1539
+ ts := ts + modifier::interval;
1540
+ EXCEPTION WHEN others THEN
1541
+ RETURN NULL;
1473
1542
  END;
1474
- $fn$;
1475
- END IF;
1543
+ RETURN __lattice_strftime_fmt(ts, format);
1544
+ END;
1545
+ $fn$;
1546
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1476
1547
  END $do$;`
1477
1548
  }
1478
1549
  ];
@@ -9391,7 +9462,12 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
9391
9462
  async function ownPolyfillsByGroup(db) {
9392
9463
  if (!isPg(db)) return;
9393
9464
  const group3 = await memberGroupFor(db);
9394
- for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
9465
+ for (const sig of [
9466
+ "json_extract(text, text)",
9467
+ "strftime(text, text)",
9468
+ "strftime(text, text, text)",
9469
+ "__lattice_strftime_fmt(timestamptz, text)"
9470
+ ]) {
9395
9471
  try {
9396
9472
  const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
9397
9473
  if (reg?.reg == null) continue;
package/dist/index.js CHANGED
@@ -909,7 +909,7 @@ async function registerPostgresPolyfills(run) {
909
909
  await run(sql);
910
910
  } catch (err) {
911
911
  const msg = err instanceof Error ? err.message : String(err);
912
- if (/permission denied/i.test(msg)) {
912
+ if (/permission denied/i.test(msg) || /must be owner of/i.test(msg)) {
913
913
  permissionDenied = true;
914
914
  } else {
915
915
  console.warn(`[PostgresAdapter] ${warn}`, msg);
@@ -1427,40 +1427,111 @@ var init_postgres = __esm({
1427
1427
  SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
1428
1428
  $fn$;
1429
1429
  END IF;
1430
+ END $do$;`
1431
+ },
1432
+ {
1433
+ warn: "could not register strftime format helper:",
1434
+ // Shared format translator (SQLite strftime tokens → to_char patterns), in UTC.
1435
+ // Factored out so the 2-arg and 3-arg strftime overloads share one definition.
1436
+ // Each polyfill below is CREATE OR REPLACE (so an existing cloud's prior copy is
1437
+ // UPGRADED when the owner opens), wrapped in a DO block whose EXCEPTION swallows a
1438
+ // scoped member's insufficient-privilege failure IN A SUBTRANSACTION — so a member
1439
+ // that runs registration inside a transaction is NOT aborted. ("must be owner of
1440
+ // function", raised when a non-owner CREATE OR REPLACEs another role's function, is
1441
+ // SQLSTATE 42501 / `insufficient_privilege`, same class as "permission denied".) The
1442
+ // owner already owns + replaced the function; the member simply uses the owner's
1443
+ // copy — and `ownPolyfillsByGroup` re-owns the whole set to the member group so any
1444
+ // member can replace them on a later upgrade.
1445
+ sql: `DO $do$ BEGIN
1446
+ CREATE OR REPLACE FUNCTION __lattice_strftime_fmt(ts timestamptz, format text)
1447
+ RETURNS text
1448
+ LANGUAGE sql
1449
+ IMMUTABLE
1450
+ AS $fn$
1451
+ SELECT to_char(
1452
+ ts AT TIME ZONE 'UTC',
1453
+ replace(replace(replace(replace(replace(replace(replace(replace(
1454
+ format,
1455
+ '%Y', 'YYYY'),
1456
+ '%m', 'MM'),
1457
+ '%d', 'DD'),
1458
+ '%H', 'HH24'),
1459
+ '%M', 'MI'),
1460
+ '%S', 'SS'),
1461
+ '%f', 'MS'),
1462
+ 'T', '"T"')
1463
+ );
1464
+ $fn$;
1465
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1430
1466
  END $do$;`
1431
1467
  },
1432
1468
  {
1433
1469
  warn: "could not register strftime polyfill:",
1470
+ // The pre-4.3.3 strftime cast the modifier straight to timestamptz and threw
1471
+ // `invalid input syntax for type timestamp with time zone: ""` on a legacy ''
1472
+ // value (3.x stored nullable TEXT timestamps as ''), bricking the whole workspace
1473
+ // open. Now an empty, whitespace, or unparseable time string returns NULL (SQLite's
1474
+ // strftime semantics) instead of aborting the query.
1434
1475
  sql: `DO $do$ BEGIN
1435
- IF to_regprocedure('strftime(text, text)') IS NULL THEN
1436
- CREATE FUNCTION strftime(format text, modifier text)
1437
- RETURNS text
1438
- LANGUAGE plpgsql
1439
- IMMUTABLE
1440
- AS $fn$
1441
- DECLARE ts timestamptz;
1442
- BEGIN
1443
- IF modifier = 'now' THEN
1444
- ts := now();
1445
- ELSE
1476
+ CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
1477
+ RETURNS text
1478
+ LANGUAGE plpgsql
1479
+ IMMUTABLE
1480
+ AS $fn$
1481
+ DECLARE ts timestamptz;
1482
+ BEGIN
1483
+ IF modifier = 'now' THEN
1484
+ ts := now();
1485
+ ELSIF NULLIF(btrim(modifier), '') IS NULL THEN
1486
+ RETURN NULL;
1487
+ ELSE
1488
+ BEGIN
1446
1489
  ts := modifier::timestamptz;
1447
- END IF;
1448
- RETURN to_char(
1449
- ts AT TIME ZONE 'UTC',
1450
- replace(replace(replace(replace(replace(replace(replace(replace(
1451
- format,
1452
- '%Y', 'YYYY'),
1453
- '%m', 'MM'),
1454
- '%d', 'DD'),
1455
- '%H', 'HH24'),
1456
- '%M', 'MI'),
1457
- '%S', 'SS'),
1458
- '%f', 'MS'),
1459
- 'T', '"T"')
1460
- );
1490
+ EXCEPTION WHEN others THEN
1491
+ RETURN NULL;
1492
+ END;
1493
+ END IF;
1494
+ RETURN __lattice_strftime_fmt(ts, format);
1495
+ END;
1496
+ $fn$;
1497
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1498
+ END $do$;`
1499
+ },
1500
+ {
1501
+ warn: "could not register 3-arg strftime polyfill:",
1502
+ // SQLite's 3-arg strftime(format, timestring, modifier) — used by the changelog
1503
+ // retention prune (e.g. strftime('%Y-...','now','-30 days')). Postgres had no
1504
+ // 3-arg overload, so that prune threw `function strftime(...) does not exist` on
1505
+ // every PG cloud with retention. Resolve the base time, apply the modifier as an
1506
+ // interval, then reuse the shared formatter. Empty/invalid → NULL, never throws.
1507
+ sql: `DO $do$ BEGIN
1508
+ CREATE OR REPLACE FUNCTION strftime(format text, timestring text, modifier text)
1509
+ RETURNS text
1510
+ LANGUAGE plpgsql
1511
+ IMMUTABLE
1512
+ AS $fn$
1513
+ DECLARE ts timestamptz;
1514
+ BEGIN
1515
+ IF timestring = 'now' THEN
1516
+ ts := now();
1517
+ ELSIF NULLIF(btrim(timestring), '') IS NULL THEN
1518
+ RETURN NULL;
1519
+ ELSE
1520
+ BEGIN
1521
+ ts := timestring::timestamptz;
1522
+ EXCEPTION WHEN others THEN
1523
+ RETURN NULL;
1524
+ END;
1525
+ END IF;
1526
+ BEGIN
1527
+ ts := ts + modifier::interval;
1528
+ EXCEPTION WHEN others THEN
1529
+ RETURN NULL;
1461
1530
  END;
1462
- $fn$;
1463
- END IF;
1531
+ RETURN __lattice_strftime_fmt(ts, format);
1532
+ END;
1533
+ $fn$;
1534
+ EXCEPTION WHEN insufficient_privilege THEN NULL;
1464
1535
  END $do$;`
1465
1536
  }
1466
1537
  ];
@@ -9397,7 +9468,12 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
9397
9468
  async function ownPolyfillsByGroup(db) {
9398
9469
  if (!isPg(db)) return;
9399
9470
  const group3 = await memberGroupFor(db);
9400
- for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
9471
+ for (const sig of [
9472
+ "json_extract(text, text)",
9473
+ "strftime(text, text)",
9474
+ "strftime(text, text, text)",
9475
+ "__lattice_strftime_fmt(timestamptz, text)"
9476
+ ]) {
9401
9477
  try {
9402
9478
  const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
9403
9479
  if (reg?.reg == null) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "4.3.2",
3
+ "version": "4.3.3",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",