@proteinjs/db-driver-spanner 1.13.0 → 1.14.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/dist/generated/index.js +1 -1
  4. package/dist/generated/index.js.map +1 -1
  5. package/dist/generated/test/index.js +1 -1
  6. package/dist/generated/test/index.js.map +1 -1
  7. package/dist/src/SpannerConfig.d.ts +22 -0
  8. package/dist/src/SpannerConfig.d.ts.map +1 -1
  9. package/dist/src/SpannerDriver.d.ts +46 -11
  10. package/dist/src/SpannerDriver.d.ts.map +1 -1
  11. package/dist/src/SpannerDriver.js +194 -26
  12. package/dist/src/SpannerDriver.js.map +1 -1
  13. package/dist/src/SpannerLivenessMonitor.d.ts +25 -0
  14. package/dist/src/SpannerLivenessMonitor.d.ts.map +1 -1
  15. package/dist/src/SpannerLivenessMonitor.js +48 -2
  16. package/dist/src/SpannerLivenessMonitor.js.map +1 -1
  17. package/dist/test/SessionPoolExhaustion.test.d.ts +2 -0
  18. package/dist/test/SessionPoolExhaustion.test.d.ts.map +1 -0
  19. package/dist/test/SessionPoolExhaustion.test.js +230 -0
  20. package/dist/test/SessionPoolExhaustion.test.js.map +1 -0
  21. package/dist/test/SpannerLivenessMonitor.test.js +42 -0
  22. package/dist/test/SpannerLivenessMonitor.test.js.map +1 -1
  23. package/dist/test/SpannerOperationDeadline.test.d.ts +2 -0
  24. package/dist/test/SpannerOperationDeadline.test.d.ts.map +1 -0
  25. package/dist/test/SpannerOperationDeadline.test.js +227 -0
  26. package/dist/test/SpannerOperationDeadline.test.js.map +1 -0
  27. package/dist/test/TransactionSafety.test.d.ts +2 -0
  28. package/dist/test/TransactionSafety.test.d.ts.map +1 -0
  29. package/dist/test/TransactionSafety.test.js +325 -0
  30. package/dist/test/TransactionSafety.test.js.map +1 -0
  31. package/generated/index.ts +6 -9
  32. package/generated/test/index.ts +8 -11
  33. package/package.json +6 -5
  34. package/src/SpannerConfig.ts +22 -0
  35. package/src/SpannerDriver.ts +204 -28
  36. package/src/SpannerLivenessMonitor.ts +61 -3
  37. package/test/SessionPoolExhaustion.test.ts +127 -0
  38. package/test/SpannerLivenessMonitor.test.ts +55 -0
  39. package/test/SpannerOperationDeadline.test.ts +156 -0
  40. package/test/TransactionSafety.test.ts +157 -0
@@ -9,11 +9,10 @@ import '@proteinjs/logger';
9
9
  import '@proteinjs/reflection';
10
10
  import '@proteinjs/util';
11
11
 
12
-
13
12
  /** Generate Source Graph */
14
13
 
15
- const sourceGraph = "{\"options\":{\"directed\":true,\"multigraph\":false,\"compound\":false},\"nodes\":[{\"v\":\"@proteinjs/db-driver-spanner/TestDbDriverFactory\",\"value\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"TestDbDriverFactory\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/db/packages/drivers/spanner/test/config/TestDbDriverFactory.ts\",\"qualifiedName\":\"@proteinjs/db-driver-spanner/TestDbDriverFactory\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[],\"methods\":[{\"name\":\"getDbDriver\",\"returnType\":{\"packageName\":\"@proteinjs/db\",\"name\":\"DbDriver\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/DbDriver\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"DefaultDbDriverFactory\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/DefaultDbDriverFactory\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/db/DefaultDbDriverFactory\"},{\"v\":\"@proteinjs/db-driver-spanner/ServiceVerbsDoc\",\"value\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"ServiceVerbsDoc\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/db/packages/drivers/spanner/test/util/serviceUpdateVerbsTestTables.ts\",\"qualifiedName\":\"@proteinjs/db-driver-spanner/ServiceVerbsDoc\",\"properties\":[{\"name\":\"title\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"scope\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":true,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"members\",\"type\":{\"packageName\":\"\",\"name\":\"ReferenceArray<ServiceVerbsDoc>\",\"filePath\":null,\"qualifiedName\":\"/ReferenceArray<ServiceVerbsDoc>\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":true,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"body\",\"type\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"ServiceVerbsDocBody\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db-driver-spanner/ServiceVerbsDocBody\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":true,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[],\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"Record\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/Record\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"sourceType\":3}},{\"v\":\"@proteinjs/db/Record\"},{\"v\":\"@proteinjs/db-driver-spanner/ServiceVerbsDocTable\",\"value\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"ServiceVerbsDocTable\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/db/packages/drivers/spanner/test/util/serviceUpdateVerbsTestTables.ts\",\"qualifiedName\":\"@proteinjs/db-driver-spanner/ServiceVerbsDocTable\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"name\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"columns\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[],\"typeParameters\":[],\"directParentInterfaces\":[],\"directParentClasses\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"Table\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/Table\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[],\"methods\":[],\"typeParameters\":[\"@proteinjs/db-driver-spanner/ServiceVerbsDoc\"],\"directParentInterfaces\":[],\"directParentClasses\":[]}],\"sourceType\":2}},{\"v\":\"@proteinjs/db/Table\"},{\"v\":\"@proteinjs/db-driver-spanner/SpannerDriver\",\"value\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"SpannerDriver\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/db/packages/drivers/spanner/src/SpannerDriver.ts\",\"qualifiedName\":\"@proteinjs/db-driver-spanner/SpannerDriver\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"SPANNER\",\"type\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Spanner\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Spanner\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"SPANNER_INSTANCE\",\"type\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Instance\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Instance\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"SPANNER_DB\",\"type\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Database\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Database\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"LIVENESS_MONITOR\",\"type\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"SpannerLivenessMonitor\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db-driver-spanner/SpannerLivenessMonitor\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"logger\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\"},{\"name\":\"config\",\"type\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"SpannerConfig\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db-driver-spanner/SpannerConfig\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\"},{\"name\":\"getTable\",\"type\":{\"packageName\":\"\",\"name\":\"((name: string) => Table<any>) | undefined\",\"filePath\":null,\"qualifiedName\":\"/((name: string) => Table<any>) | undefined\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"getSpanner\",\"returnType\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Spanner\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Spanner\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]},{\"name\":\"getSpannerInstance\",\"returnType\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Instance\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Instance\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]},{\"name\":\"getSpannerDb\",\"returnType\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Database\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Database\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]},{\"name\":\"getDbName\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"getTableManager\",\"returnType\":{\"packageName\":\"@proteinjs/db\",\"name\":\"TableManager\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/TableManager\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"getColumnType\",\"returnType\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"tableName\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"columnName\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"handleCaseSensitivity\",\"returnType\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"tableName\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"columnName\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"caseSensitive\",\"type\":{\"packageName\":\"\",\"name\":\"boolean\",\"filePath\":null,\"qualifiedName\":\"/boolean\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"createDbIfNotExists\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"dbExists\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<boolean>\",\"filePath\":null,\"qualifiedName\":\"/Promise<boolean>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"databaseName\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"runQuery\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<any[]>\",\"filePath\":null,\"qualifiedName\":\"/Promise<any[]>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"generateStatement\",\"type\":{\"packageName\":\"\",\"name\":\"(config: DbDriverQueryStatementConfig) => Statement\",\"filePath\":null,\"qualifiedName\":\"/(config: DbDriverQueryStatementConfig) => Statement\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"transaction\",\"type\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Transaction\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Transaction\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"executeQuery\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<any[]>\",\"filePath\":null,\"qualifiedName\":\"/Promise<any[]>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"generateStatement\",\"type\":{\"packageName\":\"\",\"name\":\"(config: DbDriverQueryStatementConfig) => Statement\",\"filePath\":null,\"qualifiedName\":\"/(config: DbDriverQueryStatementConfig) => Statement\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"runner\",\"type\":{\"packageName\":\"\",\"name\":\"Database | Transaction\",\"filePath\":null,\"qualifiedName\":\"/Database | Transaction\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"runDml\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<number>\",\"filePath\":null,\"qualifiedName\":\"/Promise<number>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"generateStatement\",\"type\":{\"packageName\":\"\",\"name\":\"(config: DbDriverDmlStatementConfig) => Statement\",\"filePath\":null,\"qualifiedName\":\"/(config: DbDriverDmlStatementConfig) => Statement\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"transaction\",\"type\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Transaction\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Transaction\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"executeDml\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<number>\",\"filePath\":null,\"qualifiedName\":\"/Promise<number>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"generateStatement\",\"type\":{\"packageName\":\"\",\"name\":\"(config: DbDriverDmlStatementConfig) => Statement\",\"filePath\":null,\"qualifiedName\":\"/(config: DbDriverDmlStatementConfig) => Statement\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"runner\",\"type\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Transaction\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Transaction\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"runTransaction\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<T>\",\"filePath\":null,\"qualifiedName\":\"/Promise<T>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"fn\",\"type\":{\"packageName\":\"\",\"name\":\"(transaction: Transaction) => Promise<T>\",\"filePath\":null,\"qualifiedName\":\"/(transaction: Transaction) => Promise<T>\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"rollbackQuietly\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"transaction\",\"type\":{\"packageName\":\"@google-cloud/spanner\",\"name\":\"Transaction\",\"filePath\":null,\"qualifiedName\":\"@google-cloud/spanner/Transaction\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"logIfStalled\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<T>\",\"filePath\":null,\"qualifiedName\":\"/Promise<T>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"op\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"sql\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"promise\",\"type\":{\"packageName\":\"\",\"name\":\"PromiseLike<T>\",\"filePath\":null,\"qualifiedName\":\"/PromiseLike<T>\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"timeoutMs\",\"type\":null}]},{\"name\":\"runUpdateSchema\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"sql\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"DbDriver\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/DbDriver\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/db/DbDriver\"},{\"v\":\"@proteinjs/db-driver-spanner/SpannerSchemaMetadata\",\"value\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"SpannerSchemaMetadata\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/db/packages/drivers/spanner/src/SpannerSchemaMetadata.ts\",\"qualifiedName\":\"@proteinjs/db-driver-spanner/SpannerSchemaMetadata\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[],\"methods\":[{\"name\":\"tableExists\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<boolean>\",\"filePath\":null,\"qualifiedName\":\"/Promise<boolean>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getColumnMetadata\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<{ [columnName: string]: { type: string; isNullable: boolean } }>\",\"filePath\":null,\"qualifiedName\":\"/Promise<{ [columnName: string]: { type: string; isNullable: boolean } }>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getPrimaryKey\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<string[]>\",\"filePath\":null,\"qualifiedName\":\"/Promise<string[]>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getForeignKeys\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<{ [columnName: string]: { referencedTableName: string; referencedColumnName: string } }>\",\"filePath\":null,\"qualifiedName\":\"/Promise<{ [columnName: string]: { referencedTableName: string; referencedColumnName: string } }>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getUniqueColumns\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<string[]>\",\"filePath\":null,\"qualifiedName\":\"/Promise<string[]>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getIndexes\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<{ [keyName: string]: string[] }>\",\"filePath\":null,\"qualifiedName\":\"/Promise<{ [keyName: string]: string[] }>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[],\"directParentInterfaces\":[],\"directParentClasses\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"SchemaMetadata\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/SchemaMetadata\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParentInterfaces\":[],\"directParentClasses\":[]}],\"sourceType\":2}},{\"v\":\"@proteinjs/db/SchemaMetadata\"},{\"v\":\"@proteinjs/db-driver-spanner/SpannerSchemaOperations\",\"value\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"SpannerSchemaOperations\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/db/packages/drivers/spanner/src/SpannerSchemaOperations.ts\",\"qualifiedName\":\"@proteinjs/db-driver-spanner/SpannerSchemaOperations\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"logger\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\"},{\"name\":\"spannerDriver\",\"type\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"SpannerDriver\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db-driver-spanner/SpannerDriver\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\"}],\"methods\":[{\"name\":\"createTable\",\"returnType\":null,\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"alterTable\",\"returnType\":null,\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"tableChanges\",\"type\":{\"packageName\":\"@proteinjs/db\",\"name\":\"TableChanges\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/TableChanges\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"stringWideningStatement\",\"returnType\":{\"packageName\":\"\",\"name\":\"string | null\",\"filePath\":null,\"qualifiedName\":\"/string | null\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"table\",\"type\":{\"packageName\":\"\",\"name\":\"Table<any>\",\"filePath\":null,\"qualifiedName\":\"/Table<any>\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"change\",\"type\":{\"packageName\":\"@proteinjs/db-driver-spanner\",\"name\":\"{ name: string; newType: string; oldType: string }\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db-driver-spanner/{ name: string; newType: string; oldType: string }\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"SchemaOperations\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/SchemaOperations\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/db/SchemaOperations\"}],\"edges\":[{\"v\":\"@proteinjs/db-driver-spanner/TestDbDriverFactory\",\"w\":\"@proteinjs/db/DefaultDbDriverFactory\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/db-driver-spanner/ServiceVerbsDoc\",\"w\":\"@proteinjs/db/Record\",\"value\":\"extends interface\"},{\"v\":\"@proteinjs/db-driver-spanner/ServiceVerbsDocTable\",\"w\":\"@proteinjs/db/Table\",\"value\":\"extends class\"},{\"v\":\"@proteinjs/db-driver-spanner/SpannerDriver\",\"w\":\"@proteinjs/db/DbDriver\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/db-driver-spanner/SpannerSchemaMetadata\",\"w\":\"@proteinjs/db/SchemaMetadata\",\"value\":\"extends class\"},{\"v\":\"@proteinjs/db-driver-spanner/SpannerSchemaOperations\",\"w\":\"@proteinjs/db/SchemaOperations\",\"value\":\"implements interface\"}]}";
16
-
14
+ const sourceGraph =
15
+ '{"options":{"directed":true,"multigraph":false,"compound":false},"nodes":[{"v":"@proteinjs/db-driver-spanner/TestDbDriverFactory","value":{"packageName":"@proteinjs/db-driver-spanner","name":"TestDbDriverFactory","filePath":"/home/runner/work/db/db/packages/drivers/spanner/test/config/TestDbDriverFactory.ts","qualifiedName":"@proteinjs/db-driver-spanner/TestDbDriverFactory","isAbstract":false,"isStatic":false,"visibility":"public","properties":[],"methods":[{"name":"getDbDriver","returnType":{"packageName":"@proteinjs/db","name":"DbDriver","filePath":null,"qualifiedName":"@proteinjs/db/DbDriver","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/db","name":"DefaultDbDriverFactory","filePath":null,"qualifiedName":"@proteinjs/db/DefaultDbDriverFactory","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/db/DefaultDbDriverFactory"},{"v":"@proteinjs/db-driver-spanner/ServiceVerbsDoc","value":{"packageName":"@proteinjs/db-driver-spanner","name":"ServiceVerbsDoc","filePath":"/home/runner/work/db/db/packages/drivers/spanner/test/util/serviceUpdateVerbsTestTables.ts","qualifiedName":"@proteinjs/db-driver-spanner/ServiceVerbsDoc","properties":[{"name":"title","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"scope","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"members","type":{"packageName":"","name":"ReferenceArray<ServiceVerbsDoc>","filePath":null,"qualifiedName":"/ReferenceArray<ServiceVerbsDoc>","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"body","type":{"packageName":"@proteinjs/db-driver-spanner","name":"ServiceVerbsDocBody","filePath":null,"qualifiedName":"@proteinjs/db-driver-spanner/ServiceVerbsDocBody","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[],"typeParameters":[],"directParents":[{"packageName":"@proteinjs/db","name":"Record","filePath":null,"qualifiedName":"@proteinjs/db/Record","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"sourceType":3}},{"v":"@proteinjs/db/Record"},{"v":"@proteinjs/db-driver-spanner/ServiceVerbsDocTable","value":{"packageName":"@proteinjs/db-driver-spanner","name":"ServiceVerbsDocTable","filePath":"/home/runner/work/db/db/packages/drivers/spanner/test/util/serviceUpdateVerbsTestTables.ts","qualifiedName":"@proteinjs/db-driver-spanner/ServiceVerbsDocTable","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"name","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"columns","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[],"typeParameters":[],"directParentInterfaces":[],"directParentClasses":[{"packageName":"@proteinjs/db","name":"Table","filePath":null,"qualifiedName":"@proteinjs/db/Table","isAbstract":false,"isStatic":false,"visibility":"public","properties":[],"methods":[],"typeParameters":["@proteinjs/db-driver-spanner/ServiceVerbsDoc"],"directParentInterfaces":[],"directParentClasses":[]}],"sourceType":2}},{"v":"@proteinjs/db/Table"},{"v":"@proteinjs/db-driver-spanner/SpannerDriver","value":{"packageName":"@proteinjs/db-driver-spanner","name":"SpannerDriver","filePath":"/home/runner/work/db/db/packages/drivers/spanner/src/SpannerDriver.ts","qualifiedName":"@proteinjs/db-driver-spanner/SpannerDriver","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"SPANNER","type":{"packageName":"@google-cloud/spanner","name":"Spanner","filePath":null,"qualifiedName":"@google-cloud/spanner/Spanner","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"SPANNER_INSTANCE","type":{"packageName":"@google-cloud/spanner","name":"Instance","filePath":null,"qualifiedName":"@google-cloud/spanner/Instance","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"SPANNER_DB","type":{"packageName":"@google-cloud/spanner","name":"Database","filePath":null,"qualifiedName":"@google-cloud/spanner/Database","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"LIVENESS_MONITOR","type":{"packageName":"@proteinjs/db-driver-spanner","name":"SpannerLivenessMonitor","filePath":null,"qualifiedName":"@proteinjs/db-driver-spanner/SpannerLivenessMonitor","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"CLIENT_GENERATION","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"CONSECUTIVE_DEADLINE_FAILURES","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"logger","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private"},{"name":"config","type":{"packageName":"@proteinjs/db-driver-spanner","name":"SpannerConfig","filePath":null,"qualifiedName":"@proteinjs/db-driver-spanner/SpannerConfig","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private"},{"name":"getTable","type":{"packageName":"","name":"((name: string) => Table<any>) | undefined","filePath":null,"qualifiedName":"/((name: string) => Table<any>) | undefined","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"getSpanner","returnType":{"packageName":"@google-cloud/spanner","name":"Spanner","filePath":null,"qualifiedName":"@google-cloud/spanner/Spanner","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]},{"name":"getSpannerInstance","returnType":{"packageName":"@google-cloud/spanner","name":"Instance","filePath":null,"qualifiedName":"@google-cloud/spanner/Instance","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]},{"name":"getSpannerDb","returnType":{"packageName":"@google-cloud/spanner","name":"Database","filePath":null,"qualifiedName":"@google-cloud/spanner/Database","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]},{"name":"getDbName","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"getTableManager","returnType":{"packageName":"@proteinjs/db","name":"TableManager","filePath":null,"qualifiedName":"@proteinjs/db/TableManager","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"getColumnType","returnType":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"tableName","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"columnName","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"handleCaseSensitivity","returnType":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"tableName","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"columnName","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"caseSensitive","type":{"packageName":"","name":"boolean","filePath":null,"qualifiedName":"/boolean","typeParameters":null,"directParents":null}}]},{"name":"createDbIfNotExists","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"dbExists","returnType":{"packageName":"","name":"Promise<boolean>","filePath":null,"qualifiedName":"/Promise<boolean>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"databaseName","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"runQuery","returnType":{"packageName":"","name":"Promise<any[]>","filePath":null,"qualifiedName":"/Promise<any[]>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"generateStatement","type":{"packageName":"","name":"(config: DbDriverQueryStatementConfig) => Statement","filePath":null,"qualifiedName":"/(config: DbDriverQueryStatementConfig) => Statement","typeParameters":null,"directParents":null}},{"name":"transaction","type":{"packageName":"@google-cloud/spanner","name":"Transaction","filePath":null,"qualifiedName":"@google-cloud/spanner/Transaction","typeParameters":null,"directParents":null}}]},{"name":"executeQuery","returnType":{"packageName":"","name":"Promise<any[]>","filePath":null,"qualifiedName":"/Promise<any[]>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"generateStatement","type":{"packageName":"","name":"(config: DbDriverQueryStatementConfig) => Statement","filePath":null,"qualifiedName":"/(config: DbDriverQueryStatementConfig) => Statement","typeParameters":null,"directParents":null}},{"name":"runner","type":{"packageName":"","name":"Database | Transaction","filePath":null,"qualifiedName":"/Database | Transaction","typeParameters":null,"directParents":null}}]},{"name":"runDml","returnType":{"packageName":"","name":"Promise<number>","filePath":null,"qualifiedName":"/Promise<number>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"generateStatement","type":{"packageName":"","name":"(config: DbDriverDmlStatementConfig) => Statement","filePath":null,"qualifiedName":"/(config: DbDriverDmlStatementConfig) => Statement","typeParameters":null,"directParents":null}},{"name":"transaction","type":{"packageName":"@google-cloud/spanner","name":"Transaction","filePath":null,"qualifiedName":"@google-cloud/spanner/Transaction","typeParameters":null,"directParents":null}}]},{"name":"executeDml","returnType":{"packageName":"","name":"Promise<number>","filePath":null,"qualifiedName":"/Promise<number>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"generateStatement","type":{"packageName":"","name":"(config: DbDriverDmlStatementConfig) => Statement","filePath":null,"qualifiedName":"/(config: DbDriverDmlStatementConfig) => Statement","typeParameters":null,"directParents":null}},{"name":"runner","type":{"packageName":"@google-cloud/spanner","name":"Transaction","filePath":null,"qualifiedName":"@google-cloud/spanner/Transaction","typeParameters":null,"directParents":null}}]},{"name":"runTransaction","returnType":{"packageName":"","name":"Promise<T>","filePath":null,"qualifiedName":"/Promise<T>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"fn","type":{"packageName":"","name":"(transaction: Transaction) => Promise<T>","filePath":null,"qualifiedName":"/(transaction: Transaction) => Promise<T>","typeParameters":null,"directParents":null}}]},{"name":"commit","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"transaction","type":{"packageName":"@google-cloud/spanner","name":"Transaction","filePath":null,"qualifiedName":"@google-cloud/spanner/Transaction","typeParameters":null,"directParents":null}}]},{"name":"rollbackQuietly","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"transaction","type":{"packageName":"@google-cloud/spanner","name":"Transaction","filePath":null,"qualifiedName":"@google-cloud/spanner/Transaction","typeParameters":null,"directParents":null}}]},{"name":"withDeadline","returnType":{"packageName":"","name":"Promise<T>","filePath":null,"qualifiedName":"/Promise<T>","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"op","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"sql","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"promise","type":{"packageName":"","name":"PromiseLike<T>","filePath":null,"qualifiedName":"/PromiseLike<T>","typeParameters":null,"directParents":null}}]},{"name":"recordOpSuccess","returnType":{"packageName":"","name":"void","filePath":null,"qualifiedName":"/void","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"generation","type":{"packageName":"","name":"number","filePath":null,"qualifiedName":"/number","typeParameters":null,"directParents":null}}]},{"name":"recordDeadlineFailure","returnType":{"packageName":"","name":"void","filePath":null,"qualifiedName":"/void","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"generation","type":{"packageName":"","name":"number","filePath":null,"qualifiedName":"/number","typeParameters":null,"directParents":null}}]},{"name":"recycleClient","returnType":{"packageName":"","name":"void","filePath":null,"qualifiedName":"/void","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]},{"name":"operationDeadlineMs","returnType":{"packageName":"","name":"number","filePath":null,"qualifiedName":"/number","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]},{"name":"deadlineFailuresBeforeRecycle","returnType":{"packageName":"","name":"number","filePath":null,"qualifiedName":"/number","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]},{"name":"runUpdateSchema","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"sql","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/db","name":"DbDriver","filePath":null,"qualifiedName":"@proteinjs/db/DbDriver","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/db/DbDriver"},{"v":"@proteinjs/db-driver-spanner/SpannerSchemaMetadata","value":{"packageName":"@proteinjs/db-driver-spanner","name":"SpannerSchemaMetadata","filePath":"/home/runner/work/db/db/packages/drivers/spanner/src/SpannerSchemaMetadata.ts","qualifiedName":"@proteinjs/db-driver-spanner/SpannerSchemaMetadata","isAbstract":false,"isStatic":false,"visibility":"public","properties":[],"methods":[{"name":"tableExists","returnType":{"packageName":"","name":"Promise<boolean>","filePath":null,"qualifiedName":"/Promise<boolean>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}}]},{"name":"getColumnMetadata","returnType":{"packageName":"","name":"Promise<{ [columnName: string]: { type: string; isNullable: boolean } }>","filePath":null,"qualifiedName":"/Promise<{ [columnName: string]: { type: string; isNullable: boolean } }>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}}]},{"name":"getPrimaryKey","returnType":{"packageName":"","name":"Promise<string[]>","filePath":null,"qualifiedName":"/Promise<string[]>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}}]},{"name":"getForeignKeys","returnType":{"packageName":"","name":"Promise<{ [columnName: string]: { referencedTableName: string; referencedColumnName: string } }>","filePath":null,"qualifiedName":"/Promise<{ [columnName: string]: { referencedTableName: string; referencedColumnName: string } }>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}}]},{"name":"getUniqueColumns","returnType":{"packageName":"","name":"Promise<string[]>","filePath":null,"qualifiedName":"/Promise<string[]>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}}]},{"name":"getIndexes","returnType":{"packageName":"","name":"Promise<{ [keyName: string]: string[] }>","filePath":null,"qualifiedName":"/Promise<{ [keyName: string]: string[] }>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}}]}],"typeParameters":[],"directParentInterfaces":[],"directParentClasses":[{"packageName":"@proteinjs/db","name":"SchemaMetadata","filePath":null,"qualifiedName":"@proteinjs/db/SchemaMetadata","isAbstract":false,"isStatic":false,"visibility":"public","properties":[],"methods":[],"typeParameters":[],"directParentInterfaces":[],"directParentClasses":[]}],"sourceType":2}},{"v":"@proteinjs/db/SchemaMetadata"},{"v":"@proteinjs/db-driver-spanner/SpannerSchemaOperations","value":{"packageName":"@proteinjs/db-driver-spanner","name":"SpannerSchemaOperations","filePath":"/home/runner/work/db/db/packages/drivers/spanner/src/SpannerSchemaOperations.ts","qualifiedName":"@proteinjs/db-driver-spanner/SpannerSchemaOperations","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"logger","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private"},{"name":"spannerDriver","type":{"packageName":"@proteinjs/db-driver-spanner","name":"SpannerDriver","filePath":null,"qualifiedName":"@proteinjs/db-driver-spanner/SpannerDriver","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private"}],"methods":[{"name":"createTable","returnType":null,"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}}]},{"name":"alterTable","returnType":null,"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}},{"name":"tableChanges","type":{"packageName":"@proteinjs/db","name":"TableChanges","filePath":null,"qualifiedName":"@proteinjs/db/TableChanges","typeParameters":null,"directParents":null}}]},{"name":"stringWideningStatement","returnType":{"packageName":"","name":"string | null","filePath":null,"qualifiedName":"/string | null","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"table","type":{"packageName":"","name":"Table<any>","filePath":null,"qualifiedName":"/Table<any>","typeParameters":null,"directParents":null}},{"name":"change","type":{"packageName":"@proteinjs/db-driver-spanner","name":"{ name: string; newType: string; oldType: string }","filePath":null,"qualifiedName":"@proteinjs/db-driver-spanner/{ name: string; newType: string; oldType: string }","typeParameters":null,"directParents":null}}]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/db","name":"SchemaOperations","filePath":null,"qualifiedName":"@proteinjs/db/SchemaOperations","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/db/SchemaOperations"}],"edges":[{"v":"@proteinjs/db-driver-spanner/TestDbDriverFactory","w":"@proteinjs/db/DefaultDbDriverFactory","value":"implements interface"},{"v":"@proteinjs/db-driver-spanner/ServiceVerbsDoc","w":"@proteinjs/db/Record","value":"extends interface"},{"v":"@proteinjs/db-driver-spanner/ServiceVerbsDocTable","w":"@proteinjs/db/Table","value":"extends class"},{"v":"@proteinjs/db-driver-spanner/SpannerDriver","w":"@proteinjs/db/DbDriver","value":"implements interface"},{"v":"@proteinjs/db-driver-spanner/SpannerSchemaMetadata","w":"@proteinjs/db/SchemaMetadata","value":"extends class"},{"v":"@proteinjs/db-driver-spanner/SpannerSchemaOperations","w":"@proteinjs/db/SchemaOperations","value":"implements interface"}]}';
17
16
 
18
17
  /** Generate Source Links */
19
18
 
@@ -24,18 +23,16 @@ import { SpannerSchemaMetadata } from '../../src/SpannerSchemaMetadata';
24
23
  import { SpannerSchemaOperations } from '../../src/SpannerSchemaOperations';
25
24
 
26
25
  const sourceLinks = {
27
- '@proteinjs/db-driver-spanner/TestDbDriverFactory': TestDbDriverFactory,
28
- '@proteinjs/db-driver-spanner/ServiceVerbsDocTable': ServiceVerbsDocTable,
29
- '@proteinjs/db-driver-spanner/SpannerDriver': SpannerDriver,
30
- '@proteinjs/db-driver-spanner/SpannerSchemaMetadata': SpannerSchemaMetadata,
31
- '@proteinjs/db-driver-spanner/SpannerSchemaOperations': SpannerSchemaOperations,
26
+ '@proteinjs/db-driver-spanner/TestDbDriverFactory': TestDbDriverFactory,
27
+ '@proteinjs/db-driver-spanner/ServiceVerbsDocTable': ServiceVerbsDocTable,
28
+ '@proteinjs/db-driver-spanner/SpannerDriver': SpannerDriver,
29
+ '@proteinjs/db-driver-spanner/SpannerSchemaMetadata': SpannerSchemaMetadata,
30
+ '@proteinjs/db-driver-spanner/SpannerSchemaOperations': SpannerSchemaOperations,
32
31
  };
33
32
 
34
-
35
33
  /** Load Source Graph and Links */
36
34
 
37
35
  import { SourceRepository } from '@proteinjs/reflection';
38
36
  SourceRepository.merge(sourceGraph, sourceLinks);
39
37
 
40
-
41
- export * from '../../test/index';
38
+ export * from '../../test/index';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db-driver-spanner",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "main": "./dist/generated/index.js",
5
5
  "types": "./dist/generated/index.d.ts",
6
6
  "exports": {
@@ -42,10 +42,10 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@google-cloud/spanner": "7.5.0",
45
- "@proteinjs/db": "^1.26.0",
45
+ "@proteinjs/db": "^1.27.0",
46
46
  "@proteinjs/db-query": "^1.5.0",
47
- "@proteinjs/db-spanner-common": "^1.2.0",
48
- "@proteinjs/db-transaction-context": "^0.3.0",
47
+ "@proteinjs/db-spanner-common": "^1.2.1",
48
+ "@proteinjs/db-transaction-context": "^0.4.0",
49
49
  "@proteinjs/logger": "^1.0.21",
50
50
  "@proteinjs/reflection": "^1.1.12",
51
51
  "@proteinjs/util": "^1.6.1"
@@ -62,5 +62,6 @@
62
62
  "jest": "29.7.0",
63
63
  "ts-jest": "29.1.1",
64
64
  "typescript": "5.2.2"
65
- }
65
+ },
66
+ "gitHead": "b29318b50c69f139b9f86829de1bbdf47f46a4e5"
66
67
  }
@@ -1,8 +1,30 @@
1
1
  import { SpannerOptions } from '@google-cloud/spanner';
2
+ import { SessionPoolOptions } from '@google-cloud/spanner/build/src/session-pool';
2
3
 
3
4
  export type SpannerConfig = {
4
5
  projectId: string;
5
6
  instanceName: string;
6
7
  databaseName: string;
7
8
  spannerOptions?: SpannerOptions;
9
+ /**
10
+ * Session pool options passed through to `Instance.database()` (library defaults apply when
11
+ * omitted). Note: the driver's Database is a process-wide singleton — the first driver to
12
+ * touch the db fixes the pool for the process, like `spannerOptions`.
13
+ */
14
+ sessionPoolOptions?: SessionPoolOptions;
15
+ /**
16
+ * Deadline applied to every query/dml/transaction op (default 60_000ms). An op that has not
17
+ * settled by the deadline FAILS with an error naming the deadline instead of hanging forever
18
+ * on a dead gRPC channel (the 2026-08-06 overnight wedge: Mac sleep killed the channel, every
19
+ * in-flight op hung, borrowed sessions never returned, pending ops OOM'd the heap). The same
20
+ * value is passed as the gRPC deadline (`gaxOptions.timeout`) so the library cancels the RPC
21
+ * and returns the op's session to the pool. Schema updates (DDL) are exempt — they are
22
+ * legitimately long-running.
23
+ */
24
+ operationDeadlineMs?: number;
25
+ /**
26
+ * Consecutive op-deadline failures that trigger a recycle of the process-wide Spanner
27
+ * client/channel (default 3). Any op success resets the count.
28
+ */
29
+ deadlineFailuresBeforeRecycle?: number;
8
30
  };
@@ -19,10 +19,18 @@ import { SpannerSchemaMetadata } from './SpannerSchemaMetadata';
19
19
  * Google Spanner driver for ProteinJs Db
20
20
  */
21
21
  export class SpannerDriver implements DbDriver {
22
- private static SPANNER: Spanner;
23
- private static SPANNER_INSTANCE: Instance;
24
- private static SPANNER_DB: Database;
22
+ private static SPANNER?: Spanner;
23
+ private static SPANNER_INSTANCE?: Instance;
24
+ private static SPANNER_DB?: Database;
25
25
  private static LIVENESS_MONITOR: SpannerLivenessMonitor;
26
+ /**
27
+ * Channel-death recycle accounting (2026-08-06 overnight wedge). Ops are tagged with the
28
+ * generation of the client they started on; only same-generation outcomes touch the counter,
29
+ * so a burst of deadline failures from the OLD (recycled) channel can never spuriously
30
+ * recycle the fresh one.
31
+ */
32
+ private static CLIENT_GENERATION = 0;
33
+ private static CONSECUTIVE_DEADLINE_FAILURES = 0;
26
34
  private logger = new Logger({ name: this.constructor.name });
27
35
  private config: SpannerConfig;
28
36
  public getTable: ((name: string) => Table<any>) | undefined;
@@ -75,8 +83,29 @@ export class SpannerDriver implements DbDriver {
75
83
 
76
84
  private getSpannerDb(): Database {
77
85
  if (!SpannerDriver.SPANNER_DB) {
78
- SpannerDriver.SPANNER_DB = this.getSpannerInstance().database(this.config.databaseName);
79
- SpannerDriver.LIVENESS_MONITOR = new SpannerLivenessMonitor(SpannerDriver.SPANNER_DB).start();
86
+ const db = this.getSpannerInstance().database(this.config.databaseName, this.config.sessionPoolOptions);
87
+ // The session pool forwards its 'error' events to the Database (session keepalive /
88
+ // delete failures — advisory: the pool self-heals, and per-query failures reject their
89
+ // own calls instead of flowing through this channel). Without a listener Node treats
90
+ // the emit as an unhandled 'error' event and crashes the process; a recycled client's
91
+ // post-close session deletes make that a certainty, so the owner that creates the
92
+ // Database also owns its error channel.
93
+ db.on('error', (error: Error) => {
94
+ const detail = error instanceof Error ? error.message : String(error);
95
+ if (SpannerDriver.SPANNER_DB === db) {
96
+ this.logger.warn({
97
+ message: 'Spanner session pool error (advisory; the pool self-heals)',
98
+ obj: { error: detail },
99
+ });
100
+ } else {
101
+ this.logger.debug({
102
+ message: 'Spanner session pool error from a recycled/abandoned client (expected during teardown)',
103
+ obj: { error: detail },
104
+ });
105
+ }
106
+ });
107
+ SpannerDriver.SPANNER_DB = db;
108
+ SpannerDriver.LIVENESS_MONITOR = new SpannerLivenessMonitor(db).start();
80
109
  }
81
110
 
82
111
  return SpannerDriver.SPANNER_DB;
@@ -174,13 +203,17 @@ export class SpannerDriver implements DbDriver {
174
203
 
175
204
  try {
176
205
  this.logger.debug({ message: `Executing query`, obj: { sql, params: namedParams } });
177
- const [rows] = await this.logIfStalled(
206
+ const [rows] = await this.withDeadline(
178
207
  'spanner query',
179
208
  sql,
180
209
  runner.run({
181
210
  sql,
182
211
  params: namedParams?.params,
183
212
  types: namedParams?.types,
213
+ // The gRPC deadline is what actually cancels the RPC on a dead channel: the stream
214
+ // errors, the library ends the snapshot, and the borrowed session RETURNS to the
215
+ // pool. The withDeadline race alone would fail the caller but leak the session.
216
+ gaxOptions: { timeout: this.operationDeadlineMs() },
184
217
  })
185
218
  );
186
219
  const durationMs = Number(process.hrtime.bigint() - startTime) / 1_000_000;
@@ -214,14 +247,17 @@ export class SpannerDriver implements DbDriver {
214
247
  }
215
248
 
216
249
  // Stalls in the transaction wrapper itself (session acquisition / begin / commit) happen
217
- // OUTSIDE executeDml's instrumentation — wrap the whole round trip too.
218
- return await this.logIfStalled(
250
+ // OUTSIDE executeDml's instrumentation — wrap the whole round trip too. Every await inside
251
+ // the run function is deadline-bounded (dml, commit, rollback): the run function therefore
252
+ // ALWAYS settles, which is what makes runTransactionAsync's own `finally` release the
253
+ // transaction's session back to the pool on a dead channel.
254
+ return await this.withDeadline(
219
255
  'spanner dml transaction',
220
256
  '(runTransactionAsync)',
221
257
  this.getSpannerDb().runTransactionAsync(async (transaction) => {
222
258
  try {
223
259
  const rowCount = await this.executeDml(generateStatement, transaction);
224
- await transaction.commit();
260
+ await this.commit(transaction);
225
261
  return rowCount;
226
262
  } catch (error) {
227
263
  await this.rollbackQuietly(transaction);
@@ -246,13 +282,16 @@ export class SpannerDriver implements DbDriver {
246
282
 
247
283
  try {
248
284
  this.logger.debug({ message: `Executing dml`, obj: { sql, params: namedParams } });
249
- const [rowCount] = await this.logIfStalled(
285
+ const [rowCount] = await this.withDeadline(
250
286
  'spanner dml',
251
287
  sql,
252
288
  runner.runUpdate({
253
289
  sql,
254
290
  params: namedParams?.params,
255
291
  types: namedParams?.types,
292
+ // gRPC deadline: cancels the RPC on a dead channel so the transaction's run function
293
+ // settles and the library releases the session (see runDml).
294
+ gaxOptions: { timeout: this.operationDeadlineMs() },
256
295
  })
257
296
  );
258
297
  const durationMs = Number(process.hrtime.bigint() - startTime) / 1_000_000;
@@ -278,13 +317,13 @@ export class SpannerDriver implements DbDriver {
278
317
  * @returns the return of the `fn`
279
318
  */
280
319
  async runTransaction<T>(fn: (transaction: Transaction) => Promise<T>): Promise<T> {
281
- return await this.logIfStalled(
320
+ return await this.withDeadline(
282
321
  'spanner transaction',
283
322
  '(runTransactionAsync)',
284
323
  this.getSpannerDb().runTransactionAsync(async (transaction) => {
285
324
  try {
286
325
  const result = await fn(transaction);
287
- await transaction.commit();
326
+ await this.commit(transaction);
288
327
  return result;
289
328
  } catch (error) {
290
329
  await this.rollbackQuietly(transaction);
@@ -294,6 +333,19 @@ export class SpannerDriver implements DbDriver {
294
333
  );
295
334
  }
296
335
 
336
+ /**
337
+ * Deadline-bounded commit: a commit hanging on a dead channel would otherwise keep the
338
+ * transaction's run function pending forever, and with it the session (runTransactionAsync
339
+ * only releases the session once the run function settles).
340
+ */
341
+ private async commit(transaction: Transaction): Promise<void> {
342
+ await this.withDeadline(
343
+ 'spanner commit',
344
+ '(commit)',
345
+ transaction.commit({ gaxOptions: { timeout: this.operationDeadlineMs() } })
346
+ );
347
+ }
348
+
297
349
  /**
298
350
  * Release a transaction whose work errored. The client library's runner does NOT roll back on
299
351
  * non-retryable errors — it just rethrows — so a thrown `fn` (e.g. an application rollback, or
@@ -302,36 +354,54 @@ export class SpannerDriver implements DbDriver {
302
354
  * so one leaked transaction blocks every subsequent DDL with FAILED_PRECONDITION ("a
303
355
  * read-write transaction is already in progress") — poisoning whole test runs. Rollback of an
304
356
  * already-invalid transaction (e.g. ABORTED, about to be retried by the runner with a fresh
305
- * transaction) is expected to fail; that failure is logged and swallowed so the ORIGINAL error
306
- * — the one that carries retry semantics — always propagates.
357
+ * transaction, or one whose commit was already called) is expected to fail; that failure is
358
+ * logged at debug and swallowed so the ORIGINAL error — the one that carries retry semantics —
359
+ * always propagates. Deadline-bounded like commit: an unbounded rollback on a dead channel
360
+ * would keep the run function (and its session) pending forever.
307
361
  */
308
362
  private async rollbackQuietly(transaction: Transaction): Promise<void> {
309
363
  try {
310
- await transaction.rollback();
364
+ await this.withDeadline(
365
+ 'spanner rollback',
366
+ '(rollback)',
367
+ transaction.rollback({ timeout: this.operationDeadlineMs() })
368
+ );
311
369
  } catch (rollbackError: any) {
312
370
  this.logger.debug({ message: `Rollback after transaction error failed`, obj: { rollbackError } });
313
371
  }
314
372
  }
315
373
 
316
374
  /**
317
- * Stall diagnostics (2026-07-10 flow-hang investigation): background flow tasks intermittently
318
- * wedge between model calls with every model-layer guard silent — the remaining awaits on that
319
- * path are Spanner ops, and this client has NO acquire/read timeouts (a wedged session/gRPC
320
- * stream waits forever). Logs ops still pending at 30s and 120s with the op and statement
321
- * then keeps waiting: pure diagnosis, zero behavior change.
375
+ * The one choke point every query/dml/transaction op awaits through. Two jobs:
376
+ *
377
+ * 1. Stall diagnostics (2026-07-10 flow-hang investigation): logs ops still pending at 30s
378
+ * and 120s with the op and statement.
379
+ * 2. OP DEADLINE (2026-08-06 overnight wedge): an op that has not settled by
380
+ * `operationDeadlineMs` (default 60s) FAILS with an error naming the deadline instead of
381
+ * hanging forever on a dead gRPC channel. This race covers hangs the gRPC deadline cannot
382
+ * see (session-pool acquisition, library internals before the RPC starts); the
383
+ * wire-level cancellation + session return is the gRPC deadline attached per call
384
+ * (`gaxOptions.timeout`, same value) — see the call sites. Deadline failures feed the
385
+ * channel-death recycle counter; any success resets it.
322
386
  */
323
- private logIfStalled<T>(op: string, sql: string, promise: PromiseLike<T>, timeoutMs = 30_000): Promise<T> {
387
+ private withDeadline<T>(op: string, sql: string, promise: PromiseLike<T>): Promise<T> {
388
+ // Pool gauge (P4a): every op passes through here, so this is where waiting-on-the-pool
389
+ // becomes visible (throttled inside the monitor). The monitor exists by now — all ops
390
+ // require getSpannerDb() first.
391
+ SpannerDriver.LIVENESS_MONITOR.logPoolPressure();
392
+ const deadlineMs = this.operationDeadlineMs();
393
+ const generation = SpannerDriver.CLIENT_GENERATION;
324
394
  let settled = false;
325
395
  const logStall = (afterMs: number) =>
326
396
  this.logger.error({
327
397
  message: `Spanner op stalled: ${op}`,
328
- obj: { afterMs, sql: String(sql).slice(0, 200) },
398
+ obj: { afterMs, sql: String(sql).slice(0, 200), pool: SpannerDriver.LIVENESS_MONITOR.poolStats() },
329
399
  });
330
400
  const t1 = setTimeout(() => {
331
401
  if (!settled) {
332
- logStall(timeoutMs);
402
+ logStall(30_000);
333
403
  }
334
- }, timeoutMs);
404
+ }, 30_000);
335
405
  const t2 = setTimeout(() => {
336
406
  if (!settled) {
337
407
  logStall(120_000);
@@ -339,11 +409,117 @@ export class SpannerDriver implements DbDriver {
339
409
  }, 120_000);
340
410
  t1.unref?.();
341
411
  t2.unref?.();
342
- return Promise.resolve(promise).finally(() => {
343
- settled = true;
344
- clearTimeout(t1);
345
- clearTimeout(t2);
412
+ return new Promise<T>((resolve, reject) => {
413
+ const clear = () => {
414
+ settled = true;
415
+ clearTimeout(t1);
416
+ clearTimeout(t2);
417
+ clearTimeout(deadlineTimer);
418
+ };
419
+ const deadlineTimer = setTimeout(() => {
420
+ if (settled) {
421
+ return;
422
+ }
423
+ clear();
424
+ this.logger.error({
425
+ message: `Spanner op exceeded its ${deadlineMs}ms deadline: ${op} — failing the op`,
426
+ obj: { sql: String(sql).slice(0, 200), pool: SpannerDriver.LIVENESS_MONITOR.poolStats() },
427
+ });
428
+ this.recordDeadlineFailure(generation);
429
+ // Deliberately no grpc `code` on this error: the liveness monitor's probe/exit
430
+ // escalation stays owned by genuine grpc errors; hang-shaped death is owned by the
431
+ // recycle counter.
432
+ reject(
433
+ new Error(
434
+ `Spanner op exceeded its ${deadlineMs}ms deadline: ${op} (configure via SpannerConfig.operationDeadlineMs)`
435
+ )
436
+ );
437
+ }, deadlineMs);
438
+ deadlineTimer.unref?.();
439
+ Promise.resolve(promise).then(
440
+ (value) => {
441
+ if (settled) {
442
+ return; // deadline already failed the caller; the library handles the late settle
443
+ }
444
+ clear();
445
+ this.recordOpSuccess(generation);
446
+ resolve(value);
447
+ },
448
+ (error) => {
449
+ if (settled) {
450
+ return;
451
+ }
452
+ clear();
453
+ // A grpc DEADLINE_EXCEEDED (code 4) is the same dead-channel signal arriving via the
454
+ // per-call gRPC deadline — classification for the one counter, not a second path.
455
+ if (error?.code === 4) {
456
+ this.recordDeadlineFailure(generation);
457
+ }
458
+ reject(error);
459
+ }
460
+ );
461
+ });
462
+ }
463
+
464
+ // ── Channel-death recycle (one owner for the process-wide client) ─────────
465
+
466
+ private recordOpSuccess(generation: number): void {
467
+ if (generation !== SpannerDriver.CLIENT_GENERATION) {
468
+ return; // outcome from a recycled client says nothing about the fresh channel
469
+ }
470
+ SpannerDriver.CONSECUTIVE_DEADLINE_FAILURES = 0;
471
+ }
472
+
473
+ private recordDeadlineFailure(generation: number): void {
474
+ if (generation !== SpannerDriver.CLIENT_GENERATION) {
475
+ return;
476
+ }
477
+ SpannerDriver.CONSECUTIVE_DEADLINE_FAILURES += 1;
478
+ if (SpannerDriver.CONSECUTIVE_DEADLINE_FAILURES < this.deadlineFailuresBeforeRecycle()) {
479
+ return;
480
+ }
481
+ // Reset BEFORE recycling: the counter and generation swap happen in this same synchronous
482
+ // frame, so a burst of ops all timing out together triggers exactly one recycle.
483
+ SpannerDriver.CONSECUTIVE_DEADLINE_FAILURES = 0;
484
+ this.recycleClient();
485
+ }
486
+
487
+ /**
488
+ * Drop the process-wide Spanner client so the next op builds a fresh client/channel — the
489
+ * categorical cure for a dead gRPC channel (Mac sleep, NAT drop) that deadlines every op.
490
+ * The old liveness monitor is stopped so its probe/exit escalation can't kill the process
491
+ * for a channel we just abandoned; LIVENESS_MONITOR itself stays pointed at it (stopped)
492
+ * until getSpannerDb() installs the new client's monitor, keeping in-flight ops' error
493
+ * paths callable.
494
+ */
495
+ private recycleClient(): void {
496
+ this.logger.error({
497
+ message: `Recycling Spanner client after ${this.deadlineFailuresBeforeRecycle()} consecutive op-deadline failures (dead gRPC channel suspected) — a fresh client/channel will be created on the next op`,
346
498
  });
499
+ SpannerDriver.CLIENT_GENERATION += 1;
500
+ const oldMonitor = SpannerDriver.LIVENESS_MONITOR;
501
+ const oldDb = SpannerDriver.SPANNER_DB;
502
+ const oldSpanner = SpannerDriver.SPANNER;
503
+ SpannerDriver.SPANNER = undefined;
504
+ SpannerDriver.SPANNER_INSTANCE = undefined;
505
+ SpannerDriver.SPANNER_DB = undefined;
506
+ oldMonitor?.stop();
507
+ // Best-effort teardown of the old client: session deletes are themselves RPCs on the very
508
+ // channel we believe is dead — the recycle must not depend on them succeeding.
509
+ void oldDb?.close().catch(() => undefined);
510
+ try {
511
+ oldSpanner?.close();
512
+ } catch {
513
+ // already torn down
514
+ }
515
+ }
516
+
517
+ private operationDeadlineMs(): number {
518
+ return this.config.operationDeadlineMs ?? 60_000;
519
+ }
520
+
521
+ private deadlineFailuresBeforeRecycle(): number {
522
+ return this.config.deadlineFailuresBeforeRecycle ?? 3;
347
523
  }
348
524
 
349
525
  /**