@aouda/client 0.1.13 → 0.1.15

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/index.js CHANGED
@@ -392,7 +392,7 @@ import { fileURLToPath } from "url";
392
392
  // package.json
393
393
  var package_default = {
394
394
  name: "@aouda/client",
395
- version: "0.1.13",
395
+ version: "0.1.15",
396
396
  description: "Official TypeScript/JavaScript client library for Aouda",
397
397
  type: "module",
398
398
  main: "./dist/index.cjs",
@@ -507,43 +507,44 @@ var AoudaResponseError = class extends AoudaError {
507
507
  }
508
508
  };
509
509
  var AoudaApiError = class extends AoudaError {
510
- constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
510
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
511
511
  super(message);
512
512
  this.code = code;
513
513
  this.statusCode = statusCode;
514
514
  this.details = details;
515
515
  this.requestId = requestId;
516
516
  this.retryAfterSeconds = retryAfterSeconds;
517
+ this.token = token;
517
518
  this.name = "AoudaApiError";
518
519
  }
519
520
  };
520
521
  var AoudaNotFoundError = class extends AoudaApiError {
521
- constructor(message, code, statusCode, details, requestId) {
522
- super(message, code, statusCode, details, requestId);
522
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
523
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
523
524
  this.name = "AoudaNotFoundError";
524
525
  }
525
526
  };
526
527
  var AoudaConflictError = class extends AoudaApiError {
527
- constructor(message, code, statusCode, details, requestId) {
528
- super(message, code, statusCode, details, requestId);
528
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
529
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
529
530
  this.name = "AoudaConflictError";
530
531
  }
531
532
  };
532
533
  var AoudaValidationError = class extends AoudaApiError {
533
- constructor(message, code, statusCode, details, requestId) {
534
- super(message, code, statusCode, details, requestId);
534
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
535
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
535
536
  this.name = "AoudaValidationError";
536
537
  }
537
538
  };
538
539
  var AoudaServerError = class extends AoudaApiError {
539
- constructor(message, code, statusCode, details, requestId, retryAfterSeconds) {
540
- super(message, code, statusCode, details, requestId, retryAfterSeconds);
540
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
541
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
541
542
  this.name = "AoudaServerError";
542
543
  }
543
544
  };
544
545
  var AoudaAuthenticationError = class extends AoudaApiError {
545
- constructor(message, code, statusCode, details, requestId) {
546
- super(message, code, statusCode, details, requestId);
546
+ constructor(message, code, statusCode, details, requestId, retryAfterSeconds, token) {
547
+ super(message, code, statusCode, details, requestId, retryAfterSeconds, token);
547
548
  this.name = "AoudaAuthenticationError";
548
549
  }
549
550
  };
@@ -1339,7 +1340,7 @@ function buildHandle(jobId, commitResp, wasResumed, transport, database, signal)
1339
1340
  rowsDurablyCommitted: commitResp.rowsLoaded,
1340
1341
  segmentsCreated: commitResp.segmentsCreated,
1341
1342
  committedAtUtc: commitResp.committedAtUtc,
1342
- walPosition: commitResp.walPosition,
1343
+ token: commitResp.token,
1343
1344
  writeConcernSatisfied: commitResp.writeConcernAchieved ?? "acknowledged",
1344
1345
  writeConcernTimedOut: commitResp.writeConcernTimedOut,
1345
1346
  wasResumed,
@@ -1465,6 +1466,7 @@ var PROTOCOL_VERSION_HEADER = "X-Aouda-Protocol-Version";
1465
1466
  var PROTOCOL_VERSION = "1";
1466
1467
  var CONTENT_TYPE_JSON2 = "application/json";
1467
1468
  var REQUEST_ID_HEADER2 = "X-Request-Id";
1469
+ var TOKEN_HEADER = "X-Aouda-Token";
1468
1470
  var ERROR_CODE_MAP = {
1469
1471
  TABLE_NOT_FOUND: AoudaNotFoundError,
1470
1472
  COLUMN_NOT_FOUND: AoudaNotFoundError,
@@ -1504,7 +1506,12 @@ var ERROR_CODE_MAP = {
1504
1506
  AUTH_IDENTITY_INVALID: AoudaValidationError,
1505
1507
  AUTH_IDENTITY_NOT_FOUND: AoudaValidationError,
1506
1508
  BULK_LOAD_TRANSFORM_INTENT_REQUIRED: AoudaValidationError,
1507
- BULK_LOAD_TRANSFORM_INTENT_CONFLICT: AoudaValidationError
1509
+ BULK_LOAD_TRANSFORM_INTENT_CONFLICT: AoudaValidationError,
1510
+ TOKEN_MALFORMED: AoudaValidationError,
1511
+ TOKEN_FOREIGN_DATABASE: AoudaValidationError,
1512
+ TOKEN_EPOCH_SUPERSEDED: AoudaConflictError,
1513
+ TOKEN_UNSATISFIED: AoudaConflictError,
1514
+ TOKEN_FETCH_PRIMARY: AoudaApiError
1508
1515
  };
1509
1516
  function createComposedAbortController(...signals) {
1510
1517
  const controller = new AbortController();
@@ -1541,7 +1548,7 @@ function createApiError(statusCode, statusText, body, retryAfterHeader) {
1541
1548
  const requestId = body.requestId;
1542
1549
  const retryAfterSeconds = parseRetryAfterSeconds(retryAfterHeader ?? null);
1543
1550
  const Ctor = ERROR_CODE_MAP[code] ?? AoudaApiError;
1544
- return new Ctor(message, code, statusCode, details, requestId, retryAfterSeconds);
1551
+ return new Ctor(message, code, statusCode, details, requestId, retryAfterSeconds, body.token);
1545
1552
  }
1546
1553
  var HttpTransport = class {
1547
1554
  constructor(options) {
@@ -1553,6 +1560,29 @@ var HttpTransport = class {
1553
1560
  ...options.defaultHeaders
1554
1561
  };
1555
1562
  this.abortController = new AbortController();
1563
+ this.database = options.database;
1564
+ this.store = options.consistencyTokenStore;
1565
+ }
1566
+ presentTokenHeaders(path4, headers) {
1567
+ if (this.store == null || this.database == null || isAuthPath(path4)) {
1568
+ return;
1569
+ }
1570
+ const token = this.store.get(this.database);
1571
+ if (token != null && token.length > 0) {
1572
+ headers[TOKEN_HEADER] = token;
1573
+ }
1574
+ }
1575
+ observeResponse(_path, response, bodyText, errorCode) {
1576
+ if (this.store == null || this.database == null) {
1577
+ return;
1578
+ }
1579
+ if (errorCode === "TOKEN_MALFORMED" || errorCode === "TOKEN_FOREIGN_DATABASE") {
1580
+ return;
1581
+ }
1582
+ const header = response.headers.get(TOKEN_HEADER);
1583
+ const bodyToken = readBodyToken(bodyText);
1584
+ const token = header != null && header.trim().length > 0 ? header : bodyToken;
1585
+ this.store.observe(this.database, token);
1556
1586
  }
1557
1587
  /**
1558
1588
  * Abort all in-flight and future requests (e.g. on client disconnect).
@@ -1590,6 +1620,7 @@ var HttpTransport = class {
1590
1620
  ...this.defaultHeaders,
1591
1621
  ...config.headers
1592
1622
  };
1623
+ this.presentTokenHeaders(config.path, headers);
1593
1624
  if (config.body !== void 0 && config.body !== null) {
1594
1625
  headers["Content-Type"] = CONTENT_TYPE_JSON2;
1595
1626
  }
@@ -1609,6 +1640,7 @@ var HttpTransport = class {
1609
1640
  const text = await response.text();
1610
1641
  if (config.allowStatuses?.includes(response.status) && text) {
1611
1642
  try {
1643
+ this.observeResponse(config.path, response, text);
1612
1644
  return JSON.parse(text);
1613
1645
  } catch {
1614
1646
  }
@@ -1622,15 +1654,19 @@ var HttpTransport = class {
1622
1654
  }
1623
1655
  if (response.status === 401) {
1624
1656
  const code = errorBody?.code ?? "AUTH_TOKEN_MISSING";
1657
+ this.observeResponse(config.path, response, text, code);
1625
1658
  throw new AoudaAuthenticationError(
1626
1659
  errorBody?.error ?? "Unauthorized",
1627
1660
  code,
1628
1661
  401,
1629
1662
  errorBody?.details,
1630
- errorBody?.requestId
1663
+ errorBody?.requestId,
1664
+ void 0,
1665
+ errorBody?.token
1631
1666
  );
1632
1667
  }
1633
1668
  if (errorBody?.code != null) {
1669
+ this.observeResponse(config.path, response, text, errorBody.code);
1634
1670
  throw createApiError(
1635
1671
  response.status,
1636
1672
  response.statusText,
@@ -1638,6 +1674,7 @@ var HttpTransport = class {
1638
1674
  response.headers.get("Retry-After")
1639
1675
  );
1640
1676
  }
1677
+ this.observeResponse(config.path, response, text);
1641
1678
  throw new AoudaResponseError(
1642
1679
  errorBody?.error ?? `${response.status} ${response.statusText}`,
1643
1680
  response.status,
@@ -1645,6 +1682,7 @@ var HttpTransport = class {
1645
1682
  );
1646
1683
  }
1647
1684
  const responseText = await response.text();
1685
+ this.observeResponse(config.path, response, responseText);
1648
1686
  if (config.rawText) {
1649
1687
  return responseText;
1650
1688
  }
@@ -1705,6 +1743,7 @@ var HttpTransport = class {
1705
1743
  ...this.defaultHeaders,
1706
1744
  ...config.headers
1707
1745
  };
1746
+ this.presentTokenHeaders(config.path, headers);
1708
1747
  if (config.rawBodyStr === void 0 && config.body !== void 0 && config.body !== null) {
1709
1748
  headers["Content-Type"] = CONTENT_TYPE_JSON2;
1710
1749
  }
@@ -1721,6 +1760,7 @@ var HttpTransport = class {
1721
1760
  try {
1722
1761
  const response = await localNetworkFetch(url, init);
1723
1762
  if (response.ok || config.allowStatuses?.includes(response.status)) {
1763
+ this.observeResponse(config.path, response, void 0);
1724
1764
  return response;
1725
1765
  }
1726
1766
  const text = await response.text();
@@ -1733,15 +1773,19 @@ var HttpTransport = class {
1733
1773
  }
1734
1774
  if (response.status === 401) {
1735
1775
  const code = errorBody?.code ?? "AUTH_TOKEN_MISSING";
1776
+ this.observeResponse(config.path, response, text, code);
1736
1777
  throw new AoudaAuthenticationError(
1737
1778
  errorBody?.error ?? "Unauthorized",
1738
1779
  code,
1739
1780
  401,
1740
1781
  errorBody?.details,
1741
- errorBody?.requestId
1782
+ errorBody?.requestId,
1783
+ void 0,
1784
+ errorBody?.token
1742
1785
  );
1743
1786
  }
1744
1787
  if (errorBody?.code != null) {
1788
+ this.observeResponse(config.path, response, text, errorBody.code);
1745
1789
  throw createApiError(
1746
1790
  response.status,
1747
1791
  response.statusText,
@@ -1749,6 +1793,7 @@ var HttpTransport = class {
1749
1793
  response.headers.get("Retry-After")
1750
1794
  );
1751
1795
  }
1796
+ this.observeResponse(config.path, response, text);
1752
1797
  throw new AoudaResponseError(
1753
1798
  errorBody?.error ?? `${response.status} ${response.statusText}`,
1754
1799
  response.status,
@@ -1903,6 +1948,47 @@ var HttpTransport = class {
1903
1948
  });
1904
1949
  }
1905
1950
  };
1951
+ function isAuthPath(path4) {
1952
+ let p = path4;
1953
+ const q = p.indexOf("?");
1954
+ if (q >= 0) {
1955
+ p = p.slice(0, q);
1956
+ }
1957
+ if (/^https?:\/\//i.test(p)) {
1958
+ try {
1959
+ p = new URL(p).pathname;
1960
+ } catch {
1961
+ }
1962
+ }
1963
+ const lower = p.toLowerCase();
1964
+ if (lower === "/api/auth" || lower.startsWith("/api/auth/")) {
1965
+ return true;
1966
+ }
1967
+ const prefix = "/api/databases/";
1968
+ if (!lower.startsWith(prefix)) {
1969
+ return false;
1970
+ }
1971
+ const rest = lower.slice(prefix.length);
1972
+ const slash = rest.indexOf("/");
1973
+ if (slash < 0) {
1974
+ return false;
1975
+ }
1976
+ const afterDb = rest.slice(slash + 1);
1977
+ return afterDb === "auth" || afterDb.startsWith("auth/");
1978
+ }
1979
+ function readBodyToken(bodyText) {
1980
+ if (bodyText == null || bodyText.trim().length === 0) {
1981
+ return void 0;
1982
+ }
1983
+ try {
1984
+ const parsed = JSON.parse(bodyText);
1985
+ if (parsed !== null && typeof parsed === "object" && "token" in parsed && typeof parsed.token === "string") {
1986
+ return parsed.token;
1987
+ }
1988
+ } catch {
1989
+ }
1990
+ return void 0;
1991
+ }
1906
1992
 
1907
1993
  // src/resilience/retry.ts
1908
1994
  var DEFAULT_MAX_RETRIES = 3;
@@ -2335,6 +2421,48 @@ function databasePath2(db) {
2335
2421
  return `/api/databases/${encodeURIComponent(db)}`;
2336
2422
  }
2337
2423
 
2424
+ // src/consistency-token-store.ts
2425
+ var MemoryConsistencyTokenStore = class {
2426
+ constructor() {
2427
+ this.tokens = /* @__PURE__ */ new Map();
2428
+ }
2429
+ get(database) {
2430
+ if (typeof database !== "string" || database.trim().length === 0) {
2431
+ throw new Error("database must be a non-empty string");
2432
+ }
2433
+ return this.tokens.get(normalizeKey(database));
2434
+ }
2435
+ observe(database, token) {
2436
+ if (typeof database !== "string" || database.trim().length === 0) {
2437
+ throw new Error("database must be a non-empty string");
2438
+ }
2439
+ if (token == null || token.trim().length === 0) {
2440
+ return;
2441
+ }
2442
+ const key = normalizeKey(database);
2443
+ const stored = this.tokens.get(key);
2444
+ if (stored === void 0 || compareOrdinal(token, stored) > 0) {
2445
+ this.tokens.set(key, token);
2446
+ }
2447
+ }
2448
+ };
2449
+ function compareOrdinal(left, right) {
2450
+ if (left === right) return 0;
2451
+ return left < right ? -1 : 1;
2452
+ }
2453
+ function maxToken(left, right) {
2454
+ if (left == null || left.length === 0) {
2455
+ return right == null || right.length === 0 ? void 0 : right;
2456
+ }
2457
+ if (right == null || right.length === 0) {
2458
+ return left;
2459
+ }
2460
+ return compareOrdinal(left, right) >= 0 ? left : right;
2461
+ }
2462
+ function normalizeKey(database) {
2463
+ return database.toLowerCase();
2464
+ }
2465
+
2338
2466
  // src/streaming/subscription.ts
2339
2467
  var AsyncEventQueue = class {
2340
2468
  constructor() {
@@ -2391,7 +2519,7 @@ var AsyncEventQueue = class {
2391
2519
  }
2392
2520
  };
2393
2521
  var TableSubscription = class {
2394
- constructor(transport, identity, options = {}, onWarnings) {
2522
+ constructor(transport, identity, options = {}, onWarnings, store, database) {
2395
2523
  this._queue = new AsyncEventQueue();
2396
2524
  this._active = true;
2397
2525
  this._started = false;
@@ -2407,6 +2535,11 @@ var TableSubscription = class {
2407
2535
  this._onError = options.onError;
2408
2536
  this._onWarnings = onWarnings;
2409
2537
  this._conflate = options.conflate;
2538
+ this._atLeast = options.atLeast;
2539
+ this._waitMs = options.waitMs;
2540
+ this._onExceeded = options.onExceeded;
2541
+ this._store = store;
2542
+ this._database = database;
2410
2543
  this._reconnectHandlerKey = `${this.id}::reconnect`;
2411
2544
  }
2412
2545
  get lastVersion() {
@@ -2471,10 +2604,13 @@ var TableSubscription = class {
2471
2604
  id: this.id
2472
2605
  };
2473
2606
  if (this._identity.kind === "named") {
2474
- message.hash = this._identity.hash;
2607
+ message.name = this._identity.name;
2475
2608
  if (this._identity.args !== void 0) {
2476
2609
  message.args = this._identity.args;
2477
2610
  }
2611
+ if (this._identity.orderByIndex !== void 0) {
2612
+ message.orderByIndex = this._identity.orderByIndex;
2613
+ }
2478
2614
  } else {
2479
2615
  message.target = this._identity.target;
2480
2616
  if (this._identity.filter !== void 0) {
@@ -2487,8 +2623,30 @@ var TableSubscription = class {
2487
2623
  if (this._conflate !== void 0) {
2488
2624
  message.conflate = this._conflate;
2489
2625
  }
2626
+ const pin = this._resolvePin();
2627
+ if (pin !== void 0) {
2628
+ message.at_least = pin;
2629
+ }
2630
+ if (this._waitMs !== void 0) {
2631
+ message.wait_ms = this._waitMs;
2632
+ }
2633
+ if (this._onExceeded !== void 0) {
2634
+ message.on_exceeded = this._onExceeded;
2635
+ }
2490
2636
  await this._transport.send(message);
2491
2637
  }
2638
+ _resolvePin() {
2639
+ if (this._atLeast !== void 0 && this._store != null && this._database != null) {
2640
+ this._store.observe(this._database, this._atLeast);
2641
+ }
2642
+ const stored = this._store != null && this._database != null ? this._store.get(this._database) : void 0;
2643
+ return maxToken(this._atLeast, stored);
2644
+ }
2645
+ _observeToken(token) {
2646
+ if (token !== void 0 && this._store != null && this._database != null) {
2647
+ this._store.observe(this._database, token);
2648
+ }
2649
+ }
2492
2650
  _handleMessage(message) {
2493
2651
  if (!this._active) {
2494
2652
  return;
@@ -2515,6 +2673,7 @@ var TableSubscription = class {
2515
2673
  }
2516
2674
  _handleSnapshotPage(message) {
2517
2675
  this._pendingSnapshotRows.push(...message.rows);
2676
+ this._observeToken(message.token);
2518
2677
  }
2519
2678
  _handleSnapshotComplete(message) {
2520
2679
  this._lastVersion = message.version;
@@ -2523,12 +2682,20 @@ var TableSubscription = class {
2523
2682
  }
2524
2683
  const rows = this._pendingSnapshotRows;
2525
2684
  this._pendingSnapshotRows = [];
2526
- this._onSnapshot?.(rows, message.version);
2527
- this._queue.push({
2685
+ this._onSnapshot?.(rows, message.version, message.total_matches);
2686
+ const snapshot = {
2528
2687
  type: "snapshot",
2529
2688
  rows,
2530
2689
  version: message.version
2531
- });
2690
+ };
2691
+ if (message.total_matches !== void 0) {
2692
+ snapshot.totalMatches = message.total_matches;
2693
+ }
2694
+ if (message.token !== void 0) {
2695
+ snapshot.token = message.token;
2696
+ this._observeToken(message.token);
2697
+ }
2698
+ this._queue.push(snapshot);
2532
2699
  }
2533
2700
  async _handleGap(message) {
2534
2701
  this._pendingSnapshotRows = [];
@@ -2553,6 +2720,10 @@ var TableSubscription = class {
2553
2720
  if (message.values_skipped !== void 0) {
2554
2721
  event.values_skipped = message.values_skipped;
2555
2722
  }
2723
+ if (message.token !== void 0) {
2724
+ event.token = message.token;
2725
+ this._observeToken(message.token);
2726
+ }
2556
2727
  this._onChange?.(event);
2557
2728
  this._queue.push(event);
2558
2729
  }
@@ -2897,11 +3068,12 @@ var TableQuery = class _TableQuery {
2897
3068
  * @param state - Optional initial state (used for immutable chaining).
2898
3069
  * @internal Use `client.table()` to create queries.
2899
3070
  */
2900
- constructor(transport, tableName, database, state, getWebSocketTransport) {
3071
+ constructor(transport, tableName, database, state, getWebSocketTransport, store) {
2901
3072
  this.transport = transport;
2902
3073
  this.tableName = tableName;
2903
3074
  this.database = database;
2904
3075
  this.getWebSocketTransport = getWebSocketTransport;
3076
+ this.store = store;
2905
3077
  this.state = state ?? {
2906
3078
  predicates: [],
2907
3079
  groupClauses: [],
@@ -2917,6 +3089,16 @@ var TableQuery = class _TableQuery {
2917
3089
  isDistinct: false
2918
3090
  };
2919
3091
  }
3092
+ withState(state) {
3093
+ return new _TableQuery(
3094
+ this.transport,
3095
+ this.tableName,
3096
+ this.database,
3097
+ state,
3098
+ this.getWebSocketTransport,
3099
+ this.store
3100
+ );
3101
+ }
2920
3102
  where(column, operator, value) {
2921
3103
  const newPredicates = buildWherePredicates(column, operator, value);
2922
3104
  return new _TableQuery(
@@ -2927,7 +3109,8 @@ var TableQuery = class _TableQuery {
2927
3109
  ...this.state,
2928
3110
  predicates: [...this.state.predicates, ...newPredicates]
2929
3111
  },
2930
- this.getWebSocketTransport
3112
+ this.getWebSocketTransport,
3113
+ this.store
2931
3114
  );
2932
3115
  }
2933
3116
  /**
@@ -2946,7 +3129,7 @@ var TableQuery = class _TableQuery {
2946
3129
  return new _TableQuery(this.transport, this.tableName, this.database, {
2947
3130
  ...this.state,
2948
3131
  groupClauses: [...this.state.groupClauses, sub]
2949
- }, this.getWebSocketTransport);
3132
+ }, this.getWebSocketTransport, this.store);
2950
3133
  }
2951
3134
  /**
2952
3135
  * Sets the primary sort column for the query.
@@ -2971,7 +3154,7 @@ var TableQuery = class _TableQuery {
2971
3154
  return new _TableQuery(this.transport, this.tableName, this.database, {
2972
3155
  ...this.state,
2973
3156
  orderByClauses: [orderByClause]
2974
- }, this.getWebSocketTransport);
3157
+ }, this.getWebSocketTransport, this.store);
2975
3158
  }
2976
3159
  /**
2977
3160
  * Sets the primary sort column to descending order.
@@ -3017,7 +3200,7 @@ var TableQuery = class _TableQuery {
3017
3200
  return new _TableQuery(this.transport, this.tableName, this.database, {
3018
3201
  ...this.state,
3019
3202
  orderByClauses: [...this.state.orderByClauses, orderByClause]
3020
- }, this.getWebSocketTransport);
3203
+ }, this.getWebSocketTransport, this.store);
3021
3204
  }
3022
3205
  /**
3023
3206
  * Sets the maximum number of rows to return.
@@ -3034,7 +3217,7 @@ var TableQuery = class _TableQuery {
3034
3217
  return new _TableQuery(this.transport, this.tableName, this.database, {
3035
3218
  ...this.state,
3036
3219
  limitValue: count
3037
- }, this.getWebSocketTransport);
3220
+ }, this.getWebSocketTransport, this.store);
3038
3221
  }
3039
3222
  /**
3040
3223
  * Sets the number of rows to skip.
@@ -3051,7 +3234,7 @@ var TableQuery = class _TableQuery {
3051
3234
  return new _TableQuery(this.transport, this.tableName, this.database, {
3052
3235
  ...this.state,
3053
3236
  offsetValue: count
3054
- }, this.getWebSocketTransport);
3237
+ }, this.getWebSocketTransport, this.store);
3055
3238
  }
3056
3239
  /**
3057
3240
  * Requests cross-partition access for this query.
@@ -3066,10 +3249,21 @@ var TableQuery = class _TableQuery {
3066
3249
  return new _TableQuery(this.transport, this.tableName, this.database, {
3067
3250
  ...this.state,
3068
3251
  crossPartitionAccess: true
3069
- }, this.getWebSocketTransport);
3252
+ }, this.getWebSocketTransport, this.store);
3253
+ }
3254
+ /**
3255
+ * Pin this query at at least this C-1 token. Observes the token into the
3256
+ * client store (I3, sticky) and presents it on execute via `X-Aouda-Token`.
3257
+ */
3258
+ atLeast(token) {
3259
+ if (typeof token !== "string" || token.trim().length === 0) {
3260
+ throw new Error("atLeast() requires a non-empty token");
3261
+ }
3262
+ this.store?.observe(this.database, token);
3263
+ return this.withState({ ...this.state, atLeast: token });
3070
3264
  }
3071
3265
  /**
3072
- * Restricts the columns returned in the result.
3266
+ * Selects specific columns to return.
3073
3267
  * If not called, all columns are returned.
3074
3268
  *
3075
3269
  * When T is a specific row type, only keys of T are accepted as column names.
@@ -3091,7 +3285,7 @@ var TableQuery = class _TableQuery {
3091
3285
  return new _TableQuery(this.transport, this.tableName, this.database, {
3092
3286
  ...this.state,
3093
3287
  selectColumns: columns.length > 0 ? columns : null
3094
- }, this.getWebSocketTransport);
3288
+ }, this.getWebSocketTransport, this.store);
3095
3289
  }
3096
3290
  /**
3097
3291
  * Return only distinct (de-duplicated) rows for the given columns — SQL `SELECT DISTINCT`.
@@ -3121,7 +3315,7 @@ var TableQuery = class _TableQuery {
3121
3315
  ...this.state,
3122
3316
  selectColumns: columns,
3123
3317
  isDistinct: true
3124
- }, this.getWebSocketTransport);
3318
+ }, this.getWebSocketTransport, this.store);
3125
3319
  }
3126
3320
  /**
3127
3321
  * Adds server-side computed columns to the query result.
@@ -3150,7 +3344,7 @@ var TableQuery = class _TableQuery {
3150
3344
  return new _TableQuery(this.transport, this.tableName, this.database, {
3151
3345
  ...this.state,
3152
3346
  selectExprs: projections
3153
- }, this.getWebSocketTransport);
3347
+ }, this.getWebSocketTransport, this.store);
3154
3348
  }
3155
3349
  join(rightTable, leftColumnOrColumns, rightColumnOrColumns) {
3156
3350
  return this.addJoinClause(
@@ -3208,7 +3402,8 @@ var TableQuery = class _TableQuery {
3208
3402
  }
3209
3403
  ]
3210
3404
  },
3211
- this.getWebSocketTransport
3405
+ this.getWebSocketTransport,
3406
+ this.store
3212
3407
  );
3213
3408
  }
3214
3409
  /**
@@ -3244,7 +3439,8 @@ var TableQuery = class _TableQuery {
3244
3439
  ...this.state,
3245
3440
  groupByColumns: [...columns]
3246
3441
  },
3247
- this.getWebSocketTransport
3442
+ this.getWebSocketTransport,
3443
+ this.store
3248
3444
  );
3249
3445
  }
3250
3446
  /**
@@ -3278,8 +3474,11 @@ var TableQuery = class _TableQuery {
3278
3474
  onSnapshot: options.onSnapshot,
3279
3475
  onChange: options.onChange,
3280
3476
  onError: options.onError,
3281
- conflate: options.conflate
3282
- });
3477
+ conflate: options.conflate,
3478
+ atLeast: options.atLeast ?? this.state.atLeast,
3479
+ waitMs: options.waitMs,
3480
+ onExceeded: options.onExceeded
3481
+ }, void 0, this.store, this.database);
3283
3482
  subscription.start();
3284
3483
  return subscription;
3285
3484
  }
@@ -3394,7 +3593,8 @@ var TableQuery = class _TableQuery {
3394
3593
  ...this.state,
3395
3594
  joinClauses: [...this.state.joinClauses, joinClause]
3396
3595
  },
3397
- this.getWebSocketTransport
3596
+ this.getWebSocketTransport,
3597
+ this.store
3398
3598
  );
3399
3599
  }
3400
3600
  addAggregate(op, column) {
@@ -3415,7 +3615,8 @@ var TableQuery = class _TableQuery {
3415
3615
  }
3416
3616
  ]
3417
3617
  },
3418
- this.getWebSocketTransport
3618
+ this.getWebSocketTransport,
3619
+ this.store
3419
3620
  );
3420
3621
  }
3421
3622
  requireWebSocketTransport() {
@@ -3445,17 +3646,23 @@ var TableQuery = class _TableQuery {
3445
3646
  * ```
3446
3647
  */
3447
3648
  async execute() {
3649
+ if (this.state.atLeast) {
3650
+ this.store?.observe(this.database, this.state.atLeast);
3651
+ }
3448
3652
  const request = this.buildRequest();
3449
3653
  const path4 = `${databasePath2(this.database)}/query`;
3450
3654
  const response = await this.transport.post(path4, request);
3451
3655
  const rows = columnarToRows(response);
3452
3656
  const stats = response.stats;
3453
- return { rows, stats };
3657
+ return { rows, stats, token: response.token };
3454
3658
  }
3455
3659
  /**
3456
3660
  * Executes the query and returns the raw columnar JSON payload (no row-object conversion).
3457
3661
  */
3458
3662
  async toColumnar() {
3663
+ if (this.state.atLeast) {
3664
+ this.store?.observe(this.database, this.state.atLeast);
3665
+ }
3459
3666
  const request = this.buildRequest();
3460
3667
  const path4 = `${databasePath2(this.database)}/query`;
3461
3668
  return this.transport.post(path4, request);
@@ -3485,8 +3692,12 @@ var TableQuery = class _TableQuery {
3485
3692
  limitValue: 0,
3486
3693
  selectColumns: []
3487
3694
  },
3488
- this.getWebSocketTransport
3695
+ this.getWebSocketTransport,
3696
+ this.store
3489
3697
  );
3698
+ if (this.state.atLeast) {
3699
+ this.store?.observe(this.database, this.state.atLeast);
3700
+ }
3490
3701
  const request = countQuery.buildRequest();
3491
3702
  const path4 = `${databasePath2(this.database)}/query`;
3492
3703
  const response = await this.transport.post(path4, request);
@@ -3533,7 +3744,8 @@ var TableQuery = class _TableQuery {
3533
3744
  const response = await this.transport.post(path4, body);
3534
3745
  const result = {
3535
3746
  rowsInserted: response.rowsInserted,
3536
- executionMs: response.executionMs
3747
+ executionMs: response.executionMs,
3748
+ token: response.token
3537
3749
  };
3538
3750
  if (response.generatedValues !== void 0) {
3539
3751
  result.generatedValues = response.generatedValues;
@@ -3583,7 +3795,8 @@ var TableQuery = class _TableQuery {
3583
3795
  const response = await this.transport.post(path4, body);
3584
3796
  const result = {
3585
3797
  rowsInserted: response.rowsInserted,
3586
- executionMs: response.executionMs
3798
+ executionMs: response.executionMs,
3799
+ token: response.token
3587
3800
  };
3588
3801
  if (response.generatedValues !== void 0) {
3589
3802
  result.generatedValues = response.generatedValues;
@@ -3635,6 +3848,7 @@ var TableQuery = class _TableQuery {
3635
3848
  return {
3636
3849
  rowsAffected: response.rowsUpdated,
3637
3850
  executionMs: response.executionMs,
3851
+ token: response.token,
3638
3852
  ...response.rows ? { rows: columnarToRows(response.rows) } : {},
3639
3853
  ...response.rowsTruncated ? { rowsTruncated: true } : {}
3640
3854
  };
@@ -3685,6 +3899,7 @@ var TableQuery = class _TableQuery {
3685
3899
  rowsAffected: response.rowsDeleted,
3686
3900
  executionMs: response.executionMs,
3687
3901
  hasMore: response.hasMore,
3902
+ token: response.token,
3688
3903
  ...response.rows ? { rows: columnarToRows(response.rows) } : {},
3689
3904
  ...response.rowsTruncated ? { rowsTruncated: true } : {}
3690
3905
  };
@@ -3704,7 +3919,8 @@ var TableQuery = class _TableQuery {
3704
3919
  return {
3705
3920
  rowsAffected: response.rowsDeleted,
3706
3921
  executionMs: response.executionMs,
3707
- hasMore: false
3922
+ hasMore: false,
3923
+ token: response.token
3708
3924
  };
3709
3925
  }
3710
3926
  /**
@@ -3717,7 +3933,14 @@ var TableQuery = class _TableQuery {
3717
3933
  throw new Error("batch() requires a non-empty operations array");
3718
3934
  }
3719
3935
  const wireOperations = operations.map((op, index) => {
3720
- const base = new _TableQuery(this.transport, this.tableName, this.database);
3936
+ const base = new _TableQuery(
3937
+ this.transport,
3938
+ this.tableName,
3939
+ this.database,
3940
+ void 0,
3941
+ this.getWebSocketTransport,
3942
+ this.store
3943
+ );
3721
3944
  const scoped = op.where(base);
3722
3945
  const where = scoped.buildWhereClause();
3723
3946
  if (!where) {
@@ -5054,12 +5277,12 @@ function raiseDeprecationWarnings(sink, warnings) {
5054
5277
  continue;
5055
5278
  }
5056
5279
  const sunset = warning.sunsetAt != null ? ` sunsetAt=${warning.sunsetAt}` : "";
5057
- const hash = warning.hash != null && warning.hash.length > 0 ? ` hash=${warning.hash}` : "";
5280
+ const name = warning.name != null && warning.name.length > 0 ? ` name=${warning.name}` : "";
5058
5281
  sink({
5059
5282
  code: warning.code,
5060
- hash: warning.hash,
5283
+ name: warning.name,
5061
5284
  sunsetAt: warning.sunsetAt,
5062
- message: `${warning.code}:${hash}${sunset}`.trim()
5285
+ message: `${warning.code}:${name}${sunset}`.trim()
5063
5286
  });
5064
5287
  }
5065
5288
  }
@@ -5072,21 +5295,31 @@ function emptyStats() {
5072
5295
  };
5073
5296
  }
5074
5297
  var NamedQueriesApi = class {
5075
- constructor(transport, database, onWarning, getStreamingTransport) {
5298
+ constructor(transport, database, onWarning, getStreamingTransport, store) {
5076
5299
  this.transport = transport;
5077
5300
  this.database = database;
5078
5301
  this.onWarning = onWarning;
5079
5302
  this.getStreamingTransport = getStreamingTransport;
5303
+ this.store = store;
5080
5304
  }
5081
- async execute(hash, args, options) {
5082
- if (typeof hash !== "string" || hash.trim().length === 0) {
5083
- throw new Error("Named query hash must be a non-empty string");
5305
+ async execute(name, args, options) {
5306
+ if (typeof name !== "string" || name.trim().length === 0) {
5307
+ throw new Error("Named query name must be a non-empty string");
5084
5308
  }
5085
5309
  const prefix = databasePath2(this.database);
5086
- const path4 = `${prefix}/named-queries/${encodeURIComponent(hash)}/query?format=columnar`;
5310
+ const path4 = `${prefix}/named-queries/${encodeURIComponent(name)}/query?format=columnar`;
5311
+ const body = {
5312
+ args: args ?? {}
5313
+ };
5314
+ if (options?.orderByIndex !== void 0) {
5315
+ body.orderByIndex = options.orderByIndex;
5316
+ }
5317
+ if (options?.atLeast !== void 0) {
5318
+ this.store?.observe(this.database, options.atLeast);
5319
+ }
5087
5320
  const response = await this.transport.post(
5088
5321
  path4,
5089
- { args: args ?? {} },
5322
+ body,
5090
5323
  { signal: options?.signal }
5091
5324
  );
5092
5325
  const rows = columnarToRows(response);
@@ -5094,7 +5327,9 @@ var NamedQueriesApi = class {
5094
5327
  return {
5095
5328
  rows,
5096
5329
  stats: response.stats,
5097
- warnings: response.warnings
5330
+ warnings: response.warnings,
5331
+ totalMatches: response.totalMatches,
5332
+ token: response.token
5098
5333
  };
5099
5334
  }
5100
5335
  async batch(items, options) {
@@ -5112,6 +5347,9 @@ var NamedQueriesApi = class {
5112
5347
  400
5113
5348
  );
5114
5349
  }
5350
+ if (options?.atLeast !== void 0) {
5351
+ this.store?.observe(this.database, options.atLeast);
5352
+ }
5115
5353
  const prefix = databasePath2(this.database);
5116
5354
  const path4 = `${prefix}/named-queries/batch?format=columnar`;
5117
5355
  const envelope = await this.transport.post(
@@ -5133,33 +5371,42 @@ var NamedQueriesApi = class {
5133
5371
  data: slot.data ?? [],
5134
5372
  rowCount: slot.rowCount ?? 0,
5135
5373
  stats: slot.stats ?? emptyStats(),
5136
- warnings: slot.warnings
5374
+ warnings: slot.warnings,
5375
+ totalMatches: slot.totalMatches,
5376
+ token: slot.token
5137
5377
  };
5138
5378
  const result = {
5139
5379
  rows: columnarToRows(columnar),
5140
5380
  stats: columnar.stats,
5141
- warnings: slot.warnings
5381
+ warnings: slot.warnings,
5382
+ totalMatches: slot.totalMatches,
5383
+ token: slot.token
5142
5384
  };
5143
5385
  raiseDeprecationWarnings(this.onWarning, slot.warnings);
5144
5386
  return { isError: false, result };
5145
5387
  });
5146
5388
  }
5147
- subscribe(hash, args, options = {}) {
5148
- if (typeof hash !== "string" || hash.trim().length === 0) {
5149
- throw new Error("Named query hash must be a non-empty string");
5389
+ subscribe(name, args, options = {}) {
5390
+ if (typeof name !== "string" || name.trim().length === 0) {
5391
+ throw new Error("Named query name must be a non-empty string");
5150
5392
  }
5151
5393
  const subscription = new TableSubscription(
5152
5394
  this.getStreamingTransport(),
5153
- { kind: "named", hash, args },
5395
+ { kind: "named", name, args, orderByIndex: options.orderByIndex },
5154
5396
  {
5155
5397
  onSnapshot: options.onSnapshot,
5156
5398
  onChange: options.onChange,
5157
5399
  onError: options.onError,
5158
- conflate: options.conflate
5400
+ conflate: options.conflate,
5401
+ atLeast: options.atLeast,
5402
+ waitMs: options.waitMs,
5403
+ onExceeded: options.onExceeded
5159
5404
  },
5160
5405
  (warnings) => {
5161
5406
  raiseDeprecationWarnings(this.onWarning, warnings);
5162
- }
5407
+ },
5408
+ this.store,
5409
+ this.database
5163
5410
  );
5164
5411
  subscription.start();
5165
5412
  return subscription;
@@ -5171,12 +5418,12 @@ var NamedMutationsApi = class {
5171
5418
  this.database = database;
5172
5419
  this.onWarning = onWarning;
5173
5420
  }
5174
- async execute(hash, args) {
5175
- if (typeof hash !== "string" || hash.trim().length === 0) {
5176
- throw new Error("Named mutation hash must be a non-empty string");
5421
+ async execute(name, args) {
5422
+ if (typeof name !== "string" || name.trim().length === 0) {
5423
+ throw new Error("Named mutation name must be a non-empty string");
5177
5424
  }
5178
5425
  const prefix = databasePath2(this.database);
5179
- const path4 = `${prefix}/named-mutations/${encodeURIComponent(hash)}/execute`;
5426
+ const path4 = `${prefix}/named-mutations/${encodeURIComponent(name)}/execute`;
5180
5427
  const wire = await this.transport.post(path4, {
5181
5428
  args: args ?? {}
5182
5429
  });
@@ -5237,6 +5484,7 @@ var WebSocketTransport = class {
5237
5484
  // Serialized-send queue: each send appends to this tail.
5238
5485
  this._sendTail = Promise.resolve();
5239
5486
  this._lastVersion = 0;
5487
+ this._lastToken = null;
5240
5488
  this._reconnectAttempt = 0;
5241
5489
  this._disposed = false;
5242
5490
  this._missedPings = 0;
@@ -5254,6 +5502,7 @@ var WebSocketTransport = class {
5254
5502
  this._pingIntervalMs = options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
5255
5503
  this._maxMissedHeartbeats = options.maxMissedHeartbeats ?? DEFAULT_MAX_MISSED_HEARTBEATS;
5256
5504
  this._enableCompression = options.enableCompression ?? true;
5505
+ this._store = options.consistencyTokenStore;
5257
5506
  this._wireMode = options.wireMode ?? "json";
5258
5507
  this._authHandler?.setOnAccessTokenRefreshed((token) => {
5259
5508
  if (this._handshakeComplete && this._ws?.readyState === WS_READY_STATE_OPEN) {
@@ -5262,10 +5511,14 @@ var WebSocketTransport = class {
5262
5511
  });
5263
5512
  }
5264
5513
  // ─── Public API ────────────────────────────────────────────────────────────
5265
- /** Latest version number from the most recent server `heartbeat` message. */
5514
+ /** Latest change-event sequence from the most recent server `heartbeat` message. */
5266
5515
  get lastVersion() {
5267
5516
  return this._lastVersion;
5268
5517
  }
5518
+ /** Consistency token from the most recent server `heartbeat` (ADR 0042 D-13). */
5519
+ get lastToken() {
5520
+ return this._lastToken;
5521
+ }
5269
5522
  /**
5270
5523
  * Registers a handler for incoming server messages with the given channel id.
5271
5524
  * Use `"__global__"` to receive broadcast messages that carry no `id`.
@@ -5453,6 +5706,10 @@ var WebSocketTransport = class {
5453
5706
  }
5454
5707
  case "heartbeat":
5455
5708
  this._lastVersion = msg.version;
5709
+ this._lastToken = msg.token ?? null;
5710
+ if (this._lastToken != null && this._lastToken.length > 0) {
5711
+ this._store?.observe(this._database, this._lastToken);
5712
+ }
5456
5713
  break;
5457
5714
  case "pong":
5458
5715
  this._missedPings = 0;
@@ -5618,16 +5875,21 @@ var LongPollTransport = class {
5618
5875
  this._connected = false;
5619
5876
  this._pollTask = null;
5620
5877
  this._lastVersion = 0;
5878
+ this._lastToken = null;
5621
5879
  this._serverUrl = serverUrl;
5622
5880
  this._database = database;
5623
5881
  this._authHandler = options.authHandler;
5624
5882
  this._onReconnected = options.onReconnected;
5625
5883
  this._waitMs = Math.max(1, options.waitMs ?? DEFAULT_WAIT_MS);
5626
5884
  this._fetch = options.fetchImpl ?? localNetworkFetch;
5885
+ this._store = options.consistencyTokenStore;
5627
5886
  }
5628
5887
  get lastVersion() {
5629
5888
  return this._lastVersion;
5630
5889
  }
5890
+ get lastToken() {
5891
+ return this._lastToken;
5892
+ }
5631
5893
  registerHandler(id, handler) {
5632
5894
  this._handlers.set(id, handler);
5633
5895
  }
@@ -5750,6 +6012,10 @@ var LongPollTransport = class {
5750
6012
  for (const msg of payload.messages) {
5751
6013
  if (msg.type === "heartbeat") {
5752
6014
  this._lastVersion = msg.version;
6015
+ this._lastToken = msg.token ?? null;
6016
+ if (this._lastToken != null && this._lastToken.length > 0) {
6017
+ this._store?.observe(this._database, this._lastToken);
6018
+ }
5753
6019
  }
5754
6020
  const id = "id" in msg ? msg.id : void 0;
5755
6021
  if (id !== void 0 && id !== null) {
@@ -5829,6 +6095,9 @@ var FallbackStreamingTransport = class {
5829
6095
  get lastVersion() {
5830
6096
  return this._active.lastVersion;
5831
6097
  }
6098
+ get lastToken() {
6099
+ return this._active.lastToken;
6100
+ }
5832
6101
  registerHandler(id, handler) {
5833
6102
  this._handlers.set(id, handler);
5834
6103
  this._active.registerHandler(id, handler);
@@ -5908,6 +6177,7 @@ var AoudaClient = class {
5908
6177
  this.baseUrl = normalizeBaseUrl(options.serverUrl);
5909
6178
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
5910
6179
  this.database = options.database.trim();
6180
+ this._store = options.consistencyTokenStore ?? new MemoryConsistencyTokenStore();
5911
6181
  if (!Number.isFinite(this.timeout) || this.timeout <= 0) {
5912
6182
  throw new Error("timeout must be a finite positive number");
5913
6183
  }
@@ -5942,7 +6212,9 @@ var AoudaClient = class {
5942
6212
  }
5943
6213
  const httpTransport = new HttpTransport({
5944
6214
  baseUrl: this.baseUrl,
5945
- timeout: this.timeout
6215
+ timeout: this.timeout,
6216
+ database: this.database,
6217
+ consistencyTokenStore: this._store
5946
6218
  });
5947
6219
  const activeAuth = options.serverAuth ?? options.appAuth;
5948
6220
  if (activeAuth) {
@@ -6002,7 +6274,8 @@ var AoudaClient = class {
6002
6274
  this.transport,
6003
6275
  this.database,
6004
6276
  onNamedArtifactWarning,
6005
- () => this._getOrCreateWebSocketTransport()
6277
+ () => this._getOrCreateWebSocketTransport(),
6278
+ this._store
6006
6279
  );
6007
6280
  this._namedMutations = new NamedMutationsApi(
6008
6281
  this.transport,
@@ -6101,7 +6374,8 @@ var AoudaClient = class {
6101
6374
  name,
6102
6375
  this.database,
6103
6376
  void 0,
6104
- () => this._getOrCreateWebSocketTransport()
6377
+ () => this._getOrCreateWebSocketTransport(),
6378
+ this._store
6105
6379
  );
6106
6380
  }
6107
6381
  /**
@@ -6153,13 +6427,26 @@ var AoudaClient = class {
6153
6427
  return this._materializedQueries;
6154
6428
  }
6155
6429
  /**
6156
- * Hash-only named-query execute and batch. Names are codegen aliases (D-5).
6430
+ * Named-query execute, read-only batch, and subscribe by unique schema name.
6157
6431
  */
6158
6432
  get namedQueries() {
6159
6433
  return this._namedQueries;
6160
6434
  }
6435
+ observeConsistencyToken(token) {
6436
+ this._store.observe(this.database, token);
6437
+ }
6438
+ getObservedConsistencyToken() {
6439
+ return this._store.get(this.database);
6440
+ }
6441
+ async getConsistencyToken() {
6442
+ const body = await this.transport.get(
6443
+ `/api/databases/${encodeURIComponent(this.database)}/token`
6444
+ );
6445
+ this._store.observe(this.database, body.token);
6446
+ return this._store.get(this.database) ?? body.token;
6447
+ }
6161
6448
  /**
6162
- * Hash-only named-mutation execute. No batch.
6449
+ * Named-mutation execute by unique schema name. No batch.
6163
6450
  */
6164
6451
  get namedMutations() {
6165
6452
  return this._namedMutations;
@@ -6286,13 +6573,15 @@ var AoudaClient = class {
6286
6573
  const primary = new WebSocketTransport(this.baseUrl, this.database, {
6287
6574
  authHandler: this._authHandler,
6288
6575
  enableCompression: this._streamingEnableCompression,
6289
- wireMode: this._streamingWireMode
6576
+ wireMode: this._streamingWireMode,
6577
+ consistencyTokenStore: this._store
6290
6578
  });
6291
6579
  if (this._streamingEnableLongPollFallback) {
6292
6580
  this._wsTransport = new FallbackStreamingTransport(primary, () => {
6293
6581
  return new LongPollTransport(this.baseUrl, this.database, {
6294
6582
  authHandler: this._authHandler,
6295
- waitMs: this._streamingLongPollWaitMs
6583
+ waitMs: this._streamingLongPollWaitMs,
6584
+ consistencyTokenStore: this._store
6296
6585
  });
6297
6586
  });
6298
6587
  } else {
@@ -6317,7 +6606,7 @@ Usage:
6317
6606
  npx @aouda/client schema <command> [options]
6318
6607
 
6319
6608
  Commands:
6320
- generate Fetch schema from Aouda server and output TypeScript types (tables + named query/mutation hashes)
6609
+ generate Fetch schema from Aouda server and output TypeScript types (tables + optional named query/mutation Args/Row)
6321
6610
  schema Schema management (diff, apply, export, validate, history, seed)
6322
6611
  diff Show migration plan (desired vs current)
6323
6612
  apply Apply schema (use --allow-destructive for drops; --dry-run to preview)