@stacksjs/database 0.75.131 → 0.75.133

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 CHANGED
@@ -66,3 +66,12 @@ canonical numeric string value, so padded numeric inputs cannot reverse lock ord
66
66
  transaction handle and choose ownership filters; reads, writes and validation
67
67
  remain on that handle. This prevents opposite input order from creating a
68
68
  lock-order cycle during a transfer, merge or multi-item stock reservation.
69
+
70
+ `lockRow(tx, table, scope, { mode: 'no_key_update' })` uses PostgreSQL's
71
+ `FOR NO KEY UPDATE` for parent rows whose key will stay fixed. It serializes
72
+ other non-key updates while allowing child inserts to acquire foreign-key
73
+ key-share locks. Acquire the parent before child locks consistently; a strong
74
+ `FOR UPDATE` parent lock can deadlock with a child-first insert. MySQL retains
75
+ its stronger `FOR UPDATE` lock, and SQLite relies on the surrounding transaction.
76
+ The default remains `update`; changing or deleting the parent key needs that
77
+ stronger lock. Group locks accept the same mode.
@@ -1,10 +1,13 @@
1
1
  /** Check trusted table/column names before composing a scoped operation. */
2
2
  export declare function assertSqlIdentifier(name: string): void;
3
3
  /** Lock a scoped row until the caller's transaction commits. SQLite transactions serialize writers. */
4
- export declare function lockRow(connection: { unsafe?: (query: string, params?: unknown[]) => PromiseLike<Record<string, unknown>[]> }, table: string, filters: Record<string, string | number | null>): Promise<Record<string, unknown> | undefined>;
4
+ export declare function lockRow(connection: { unsafe?: (query: string, params?: unknown[]) => PromiseLike<Record<string, unknown>[]> }, table: string, filters: Record<string, string | number | null>, options?: RowLockOptions): Promise<Record<string, unknown> | undefined>;
5
5
  /** Lock a group in one deterministic order, independent of the request's ordering. */
6
6
  export declare function lockRows(connection: Parameters<typeof lockRow>[0], table: string, ids: readonly (string | number)[], options?: RowLockGroupOptions): Promise<Map<string, Record<string, unknown> | undefined>>;
7
- export declare interface RowLockGroupOptions {
7
+ export declare interface RowLockOptions {
8
+ mode?: 'update' | 'no_key_update'
9
+ }
10
+ export declare interface RowLockGroupOptions extends RowLockOptions {
8
11
  key?: string
9
12
  scope?: Record<string, string | number | null>
10
13
  }
package/dist/row-lock.js CHANGED
@@ -1 +1 @@
1
- import{sqlHelpers}from"./sql-helpers";import{getDatabaseDialect}from"./utils";export function assertSqlIdentifier(name){if(!/^[a-z_][a-z_0-9]*$/i.test(name))throw Error(`Invalid SQL identifier: ${name}`)}export async function lockRow(connection,table,filters){const dialect=sqlHelpers(getDatabaseDialect()),quote=dialect.isMysql?"`":'"',identifier=(name)=>{assertSqlIdentifier(name);return`${quote}${name}${quote}`},values=[],conditions=Object.entries(filters).map(([name,value])=>{if(value===null)return`${identifier(name)} IS NULL`;values.push(value);return`${identifier(name)} = ${dialect.param(values.length)}`});if(!conditions.length)throw Error("A row lock requires a scope");const suffix=dialect.isPostgres||dialect.isMysql?" FOR UPDATE":"";if(!connection.unsafe)throw Error("Row locks require a database connection");const rows=await connection.unsafe(`SELECT * FROM ${identifier(table)} WHERE ${conditions.join(" AND ")}${suffix}`,values);if(rows.length>1)throw Error("A row lock requires a unique scope");return rows[0]}export async function lockRows(connection,table,ids,options={}){const key=options.key??"id";assertSqlIdentifier(table);assertSqlIdentifier(key);if(Object.prototype.hasOwnProperty.call(options.scope??{},key))throw Error("The group scope cannot replace its primary key");const unique=new Map;for(const id of ids){if(typeof id!=="number"&&(typeof id!=="string"||!/^\d+$/.test(id)))throw Error("Group locks require numeric primary keys");const number=Number(id);if(!Number.isSafeInteger(number)||number<=0)throw Error("Invalid row key");unique.set(String(number),number)}const rows=new Map;for(const name of[...unique.keys()].sort())rows.set(name,await lockRow(connection,table,{...options.scope,[key]:unique.get(name)}));return rows}
1
+ import{sqlHelpers}from"./sql-helpers";import{getDatabaseDialect}from"./utils";export function assertSqlIdentifier(name){if(!/^[a-z_][a-z_0-9]*$/i.test(name))throw Error(`Invalid SQL identifier: ${name}`)}export async function lockRow(connection,table,filters,options={}){const dialect=sqlHelpers(getDatabaseDialect()),quote=dialect.isMysql?"`":'"',identifier=(name)=>{assertSqlIdentifier(name);return`${quote}${name}${quote}`},values=[],conditions=Object.entries(filters).map(([name,value])=>{if(value===null)return`${identifier(name)} IS NULL`;values.push(value);return`${identifier(name)} = ${dialect.param(values.length)}`});if(!conditions.length)throw Error("A row lock requires a scope");const mode=options.mode??"update";if(mode!=="update"&&mode!=="no_key_update")throw Error("Unsupported row lock mode");const suffix=dialect.isPostgres&&mode==="no_key_update"?" FOR NO KEY UPDATE":dialect.isPostgres||dialect.isMysql?" FOR UPDATE":"";if(!connection.unsafe)throw Error("Row locks require a database connection");const rows=await connection.unsafe(`SELECT * FROM ${identifier(table)} WHERE ${conditions.join(" AND ")}${suffix}`,values);if(rows.length>1)throw Error("A row lock requires a unique scope");return rows[0]}export async function lockRows(connection,table,ids,options={}){const key=options.key??"id";assertSqlIdentifier(table);assertSqlIdentifier(key);if(Object.prototype.hasOwnProperty.call(options.scope??{},key))throw Error("The group scope cannot replace its primary key");const unique=new Map;for(const id of ids){if(typeof id!=="number"&&(typeof id!=="string"||!/^\d+$/.test(id)))throw Error("Group locks require numeric primary keys");const number=Number(id);if(!Number.isSafeInteger(number)||number<=0)throw Error("Invalid row key");unique.set(String(number),number)}const rows=new Map;for(const name of[...unique.keys()].sort())rows.set(name,await lockRow(connection,table,{...options.scope,[key]:unique.get(name)},{mode:options.mode}));return rows}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.131",
5
+ "version": "0.75.133",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -66,26 +66,26 @@
66
66
  "prepublishOnly": "bun run build"
67
67
  },
68
68
  "dependencies": {
69
- "@stacksjs/config": "0.75.131",
70
- "@stacksjs/env": "0.75.131",
71
- "@stacksjs/error-handling": "0.75.131",
72
- "@stacksjs/faker": "0.75.131",
73
- "@stacksjs/features": "0.75.131",
74
- "@stacksjs/logging": "0.75.131",
75
- "@stacksjs/model-meta": "0.75.131",
76
- "@stacksjs/path": "0.75.131",
77
- "@stacksjs/query-builder": "0.75.131",
78
- "@stacksjs/security": "0.75.131",
79
- "@stacksjs/storage": "0.75.131",
80
- "@stacksjs/strings": "0.75.131",
69
+ "@stacksjs/config": "0.75.133",
70
+ "@stacksjs/env": "0.75.133",
71
+ "@stacksjs/error-handling": "0.75.133",
72
+ "@stacksjs/faker": "0.75.133",
73
+ "@stacksjs/features": "0.75.133",
74
+ "@stacksjs/logging": "0.75.133",
75
+ "@stacksjs/model-meta": "0.75.133",
76
+ "@stacksjs/path": "0.75.133",
77
+ "@stacksjs/query-builder": "0.75.133",
78
+ "@stacksjs/security": "0.75.133",
79
+ "@stacksjs/storage": "0.75.133",
80
+ "@stacksjs/strings": "0.75.133",
81
81
  "@stacksjs/ts-validation": "^0.5.6",
82
82
  "bun-query-builder": "^0.3.5",
83
83
  "dynamodb-tooling": "^0.3.6"
84
84
  },
85
85
  "devDependencies": {
86
- "@stacksjs/cli": "0.75.131",
87
- "@stacksjs/router": "0.75.131",
88
- "@stacksjs/utils": "0.75.131",
86
+ "@stacksjs/cli": "0.75.133",
87
+ "@stacksjs/router": "0.75.133",
88
+ "@stacksjs/utils": "0.75.133",
89
89
  "better-dx": "^0.2.24"
90
90
  }
91
91
  }