@rocicorp/zero 1.11.0-canary.38 → 1.11.0-canary.39

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 (75) hide show
  1. package/out/shared/src/valita.d.ts +75 -0
  2. package/out/shared/src/valita.d.ts.map +1 -1
  3. package/out/shared/src/valita.js +119 -1
  4. package/out/shared/src/valita.js.map +1 -1
  5. package/out/zero/package.js +1 -1
  6. package/out/zero/package.js.map +1 -1
  7. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +2 -2
  8. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  9. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -2
  10. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  11. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  12. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  13. package/out/zero-cache/src/workers/connection.d.ts +2 -1
  14. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  15. package/out/zero-cache/src/workers/connection.js +9 -1
  16. package/out/zero-cache/src/workers/connection.js.map +1 -1
  17. package/out/zero-client/src/client/storage-version.d.ts +23 -0
  18. package/out/zero-client/src/client/storage-version.d.ts.map +1 -0
  19. package/out/zero-client/src/client/version.js +1 -1
  20. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  21. package/out/zero-client/src/client/zero.js +1 -1
  22. package/out/zero-client/src/client/zero.js.map +1 -1
  23. package/out/zero-protocol/src/analyze-query-result.d.ts +12 -12
  24. package/out/zero-protocol/src/analyze-query-result.d.ts.map +1 -1
  25. package/out/zero-protocol/src/analyze-query-result.js +3 -3
  26. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  27. package/out/zero-protocol/src/ast.d.ts +5 -0
  28. package/out/zero-protocol/src/ast.d.ts.map +1 -1
  29. package/out/zero-protocol/src/ast.js +5 -7
  30. package/out/zero-protocol/src/ast.js.map +1 -1
  31. package/out/zero-protocol/src/down.d.ts +5 -5
  32. package/out/zero-protocol/src/inspect-down.d.ts +15 -15
  33. package/out/zero-protocol/src/inspect-up.d.ts +158 -61
  34. package/out/zero-protocol/src/inspect-up.d.ts.map +1 -1
  35. package/out/zero-protocol/src/inspect-up.js +12 -6
  36. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  37. package/out/zero-protocol/src/protocol-version.d.ts +4 -1
  38. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  39. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  40. package/out/zero-protocol/src/queries-patch.js +2 -2
  41. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  42. package/out/zero-protocol/src/up.d.ts +766 -256
  43. package/out/zero-protocol/src/up.d.ts.map +1 -1
  44. package/out/zero-protocol/src/up.js +25 -3
  45. package/out/zero-protocol/src/up.js.map +1 -1
  46. package/out/zero-server/src/adapters/drizzle.d.ts +14 -3
  47. package/out/zero-server/src/adapters/drizzle.d.ts.map +1 -1
  48. package/out/zero-server/src/adapters/drizzle.js +7 -4
  49. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  50. package/out/zero-server/src/adapters/kysely.d.ts +4 -2
  51. package/out/zero-server/src/adapters/kysely.d.ts.map +1 -1
  52. package/out/zero-server/src/adapters/kysely.js +8 -4
  53. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  54. package/out/zero-server/src/adapters/pg.d.ts +3 -2
  55. package/out/zero-server/src/adapters/pg.d.ts.map +1 -1
  56. package/out/zero-server/src/adapters/pg.js +7 -4
  57. package/out/zero-server/src/adapters/pg.js.map +1 -1
  58. package/out/zero-server/src/adapters/postgresjs.d.ts +3 -2
  59. package/out/zero-server/src/adapters/postgresjs.d.ts.map +1 -1
  60. package/out/zero-server/src/adapters/postgresjs.js +8 -4
  61. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  62. package/out/zero-server/src/adapters/prisma.d.ts +14 -3
  63. package/out/zero-server/src/adapters/prisma.d.ts.map +1 -1
  64. package/out/zero-server/src/adapters/prisma.js +12 -4
  65. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  66. package/out/zero-server/src/mod.d.ts +1 -0
  67. package/out/zero-server/src/mod.d.ts.map +1 -1
  68. package/out/zero-server/src/transaction-options.d.ts +24 -0
  69. package/out/zero-server/src/transaction-options.d.ts.map +1 -0
  70. package/out/zero-server/src/transaction-options.js +11 -0
  71. package/out/zero-server/src/transaction-options.js.map +1 -0
  72. package/out/zqlite/src/sqlite-cost-model.d.ts.map +1 -1
  73. package/out/zqlite/src/sqlite-cost-model.js +1 -0
  74. package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
  75. package/package.json +1 -1
@@ -1,16 +1,24 @@
1
1
  import { executePostgresQuery } from "../pg-query-executor.js";
2
2
  import { ZQLDatabase } from "../zql-database.js";
3
3
  //#region ../zero-server/src/adapters/prisma.ts
4
+ var PRISMA_ISOLATION_LEVELS = {
5
+ "read committed": "ReadCommitted",
6
+ "repeatable read": "RepeatableRead",
7
+ "serializable": "Serializable"
8
+ };
4
9
  var PrismaConnection = class {
5
10
  #client;
6
- constructor(client) {
11
+ #options;
12
+ constructor(client, options = {}) {
7
13
  this.#client = client;
14
+ this.#options = options;
8
15
  }
9
16
  query(sql, params) {
10
17
  return prismaQuery(this.#client, sql, params);
11
18
  }
12
19
  transaction(fn) {
13
- return this.#client.$transaction((prismaTx) => fn(new PrismaInternalTransaction(prismaTx)));
20
+ const { isolationLevel } = this.#options;
21
+ return this.#client.$transaction((prismaTx) => fn(new PrismaInternalTransaction(prismaTx)), isolationLevel === void 0 ? void 0 : { isolationLevel: PRISMA_ISOLATION_LEVELS[isolationLevel] });
14
22
  }
15
23
  };
16
24
  var PrismaInternalTransaction = class {
@@ -80,8 +88,8 @@ function toIterableRows(result) {
80
88
  * });
81
89
  * ```
82
90
  */
83
- function zeroPrisma(schema, client) {
84
- return new ZQLDatabase(new PrismaConnection(client), schema);
91
+ function zeroPrisma(schema, client, options) {
92
+ return new ZQLDatabase(new PrismaConnection(client, options), schema);
85
93
  }
86
94
  //#endregion
87
95
  export { PrismaConnection, zeroPrisma };
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.js","names":["#client"],"sources":["../../../../../zero-server/src/adapters/prisma.ts"],"sourcesContent":["import type {AST} from '../../../zero-protocol/src/ast.ts';\nimport type {Format} from '../../../zero-types/src/format.ts';\nimport type {Schema} from '../../../zero-types/src/schema.ts';\nimport type {ServerSchema} from '../../../zero-types/src/server-schema.ts';\nimport type {\n DBConnection,\n DBTransaction,\n Row,\n} from '../../../zql/src/mutate/custom.ts';\nimport type {HumanReadable} from '../../../zql/src/query/query.ts';\nimport {executePostgresQuery} from '../pg-query-executor.ts';\nimport {ZQLDatabase} from '../zql-database.ts';\n\nexport type {ZQLDatabase};\n\nexport type PrismaTransactionLike = {\n $queryRawUnsafe: (query: string, ...params: unknown[]) => Promise<unknown>;\n};\n\nexport type PrismaClientLike<\n TTransaction extends PrismaTransactionLike = PrismaTransactionLike,\n> = PrismaTransactionLike & {\n $transaction: <T>(fn: (tx: TTransaction) => Promise<T>) => Promise<T>;\n};\n\n/**\n * Helper type for the wrapped transaction used by Prisma.\n *\n * @remarks Use with `ServerTransaction` as `ServerTransaction<Schema, PrismaTransaction<typeof prisma>>`.\n */\nexport type PrismaTransaction<\n TClient extends PrismaClientLike = PrismaClientLike,\n> =\n TClient extends PrismaClientLike<infer TTransaction>\n ? TTransaction\n : PrismaTransactionLike;\n\nexport class PrismaConnection<\n TClient extends PrismaClientLike,\n> implements DBConnection<PrismaTransaction<TClient>> {\n readonly #client: TClient;\n\n constructor(client: TClient) {\n this.#client = client;\n }\n\n query(sql: string, params: unknown[]): Promise<Iterable<Row>> {\n return prismaQuery(this.#client, sql, params);\n }\n\n transaction<T>(\n fn: (tx: DBTransaction<PrismaTransaction<TClient>>) => Promise<T>,\n ): Promise<T> {\n return this.#client.$transaction(prismaTx =>\n fn(\n new PrismaInternalTransaction(prismaTx) as DBTransaction<\n PrismaTransaction<TClient>\n >,\n ),\n );\n }\n}\n\nclass PrismaInternalTransaction<\n TTransaction extends PrismaTransactionLike,\n> implements DBTransaction<TTransaction> {\n readonly wrappedTransaction: TTransaction;\n\n constructor(prismaTx: TTransaction) {\n this.wrappedTransaction = prismaTx;\n }\n\n runQuery<TReturn>(\n ast: AST,\n format: Format,\n schema: Schema,\n serverSchema: ServerSchema,\n ): Promise<HumanReadable<TReturn>> {\n return executePostgresQuery<TReturn>(\n this,\n ast,\n format,\n schema,\n serverSchema,\n );\n }\n\n query(sql: string, params: unknown[]): Promise<Iterable<Row>> {\n return prismaQuery(this.wrappedTransaction, sql, params);\n }\n}\n\nasync function prismaQuery(\n client: PrismaTransactionLike,\n sql: string,\n params: unknown[],\n): Promise<Iterable<Row>> {\n const result = await client.$queryRawUnsafe(sql, ...params);\n return toIterableRows(result);\n}\n\nfunction isIterable(value: unknown): value is Iterable<unknown> {\n return (\n // oxlint-disable-next-line eqeqeq\n value != null &&\n typeof (value as Iterable<unknown>)[Symbol.iterator] === 'function'\n );\n}\n\nfunction toIterableRows(result: unknown): Iterable<Row> {\n if (result === null || result === undefined) {\n return [] as Row[];\n }\n if (Array.isArray(result)) {\n return result as Row[];\n }\n if (isIterable(result)) {\n return result as Iterable<Row>;\n }\n return [] as Row[];\n}\n\n/**\n * Wrap a Prisma client for Zero ZQL.\n *\n * Provides ZQL querying plus access to the underlying Prisma transaction.\n * Use {@link PrismaTransaction} to type your server mutator transaction.\n *\n * @param schema - Zero schema.\n * @param client - Prisma client.\n *\n * @example\n * ```ts\n * import {PrismaPg} from '@prisma/adapter-pg';\n * import {PrismaClient} from '@prisma/client';\n * import {defineMutator, defineMutators} from '@rocicorp/zero';\n * import {zeroPrisma} from '@rocicorp/zero/server/adapters/prisma';\n * import {z} from 'zod/mini';\n *\n * const prisma = new PrismaClient({\n * adapter: new PrismaPg({connectionString: process.env.ZERO_UPSTREAM_DB!}),\n * });\n * const zql = zeroPrisma(schema, prisma);\n *\n * export const serverMutators = defineMutators({\n * user: {\n * create: defineMutator(\n * z.object({id: z.string(), name: z.string()}),\n * async ({tx, args}) => {\n * if (tx.location !== 'server') {\n * throw new Error('Server-only mutator');\n * }\n * await tx.dbTransaction.wrappedTransaction.user.create({\n * data: {\n * id: args.id,\n * name: args.name,\n * status: 'active',\n * },\n * });\n * },\n * ),\n * },\n * });\n * ```\n */\nexport function zeroPrisma<\n TSchema extends Schema,\n TClient extends PrismaClientLike,\n>(\n schema: TSchema,\n client: TClient,\n): ZQLDatabase<TSchema, PrismaTransaction<TClient>> {\n return new ZQLDatabase(new PrismaConnection(client), schema);\n}\n"],"mappings":";;;AAqCA,IAAa,mBAAb,MAEsD;CACpD;CAEA,YAAY,QAAiB;EAC3B,KAAKA,UAAU;CACjB;CAEA,MAAM,KAAa,QAA2C;EAC5D,OAAO,YAAY,KAAKA,SAAS,KAAK,MAAM;CAC9C;CAEA,YACE,IACY;EACZ,OAAO,KAAKA,QAAQ,cAAa,aAC/B,GACE,IAAI,0BAA0B,QAAQ,CAGxC,CACF;CACF;AACF;AAEA,IAAM,4BAAN,MAEyC;CACvC;CAEA,YAAY,UAAwB;EAClC,KAAK,qBAAqB;CAC5B;CAEA,SACE,KACA,QACA,QACA,cACiC;EACjC,OAAO,qBACL,MACA,KACA,QACA,QACA,YACF;CACF;CAEA,MAAM,KAAa,QAA2C;EAC5D,OAAO,YAAY,KAAK,oBAAoB,KAAK,MAAM;CACzD;AACF;AAEA,eAAe,YACb,QACA,KACA,QACwB;CAExB,OAAO,eAAe,MADD,OAAO,gBAAgB,KAAK,GAAG,MAAM,CAC9B;AAC9B;AAEA,SAAS,WAAW,OAA4C;CAC9D,OAEE,SAAS,QACT,OAAQ,MAA4B,OAAO,cAAc;AAE7D;AAEA,SAAS,eAAe,QAAgC;CACtD,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,OAAO,CAAC;CAEV,IAAI,MAAM,QAAQ,MAAM,GACtB,OAAO;CAET,IAAI,WAAW,MAAM,GACnB,OAAO;CAET,OAAO,CAAC;AACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,WAId,QACA,QACkD;CAClD,OAAO,IAAI,YAAY,IAAI,iBAAiB,MAAM,GAAG,MAAM;AAC7D"}
1
+ {"version":3,"file":"prisma.js","names":["#client","#options"],"sources":["../../../../../zero-server/src/adapters/prisma.ts"],"sourcesContent":["import type {AST} from '../../../zero-protocol/src/ast.ts';\nimport type {Format} from '../../../zero-types/src/format.ts';\nimport type {Schema} from '../../../zero-types/src/schema.ts';\nimport type {ServerSchema} from '../../../zero-types/src/server-schema.ts';\nimport type {\n DBConnection,\n DBTransaction,\n Row,\n} from '../../../zql/src/mutate/custom.ts';\nimport type {HumanReadable} from '../../../zql/src/query/query.ts';\nimport {executePostgresQuery} from '../pg-query-executor.ts';\nimport type {\n IsolationLevel,\n TransactionOptions,\n} from '../transaction-options.ts';\nimport {ZQLDatabase} from '../zql-database.ts';\n\nexport type {ZQLDatabase};\n\nexport type PrismaTransactionLike = {\n $queryRawUnsafe: (query: string, ...params: unknown[]) => Promise<unknown>;\n};\n\nexport type PrismaClientLike<\n TTransaction extends PrismaTransactionLike = PrismaTransactionLike,\n> = PrismaTransactionLike & {\n $transaction: <T>(\n fn: (tx: TTransaction) => Promise<T>,\n options?: PrismaTransactionOptions | undefined,\n ) => Promise<T>;\n};\n\n/**\n * The subset of Prisma's transaction options this adapter passes through.\n *\n * Prisma spells the levels in PascalCase rather than as Postgres does, so\n * {@link PRISMA_ISOLATION_LEVELS} maps between them.\n */\ntype PrismaTransactionOptions = {\n // No `| undefined`: Prisma's own option is declared without it, and under\n // `exactOptionalPropertyTypes` that would make a real client unassignable.\n isolationLevel?: PrismaIsolationLevel;\n};\n\ntype PrismaIsolationLevel = 'ReadCommitted' | 'RepeatableRead' | 'Serializable';\n\nconst PRISMA_ISOLATION_LEVELS: Record<IsolationLevel, PrismaIsolationLevel> = {\n 'read committed': 'ReadCommitted',\n 'repeatable read': 'RepeatableRead',\n 'serializable': 'Serializable',\n};\n\n/**\n * Helper type for the wrapped transaction used by Prisma.\n *\n * @remarks Use with `ServerTransaction` as `ServerTransaction<Schema, PrismaTransaction<typeof prisma>>`.\n */\nexport type PrismaTransaction<\n TClient extends PrismaClientLike = PrismaClientLike,\n> =\n TClient extends PrismaClientLike<infer TTransaction>\n ? TTransaction\n : PrismaTransactionLike;\n\nexport class PrismaConnection<\n TClient extends PrismaClientLike,\n> implements DBConnection<PrismaTransaction<TClient>> {\n readonly #client: TClient;\n readonly #options: TransactionOptions;\n\n constructor(client: TClient, options: TransactionOptions = {}) {\n this.#client = client;\n this.#options = options;\n }\n\n query(sql: string, params: unknown[]): Promise<Iterable<Row>> {\n return prismaQuery(this.#client, sql, params);\n }\n\n transaction<T>(\n fn: (tx: DBTransaction<PrismaTransaction<TClient>>) => Promise<T>,\n ): Promise<T> {\n const {isolationLevel} = this.#options;\n return this.#client.$transaction(\n prismaTx =>\n fn(\n new PrismaInternalTransaction(prismaTx) as DBTransaction<\n PrismaTransaction<TClient>\n >,\n ),\n isolationLevel === undefined\n ? undefined\n : {isolationLevel: PRISMA_ISOLATION_LEVELS[isolationLevel]},\n );\n }\n}\n\nclass PrismaInternalTransaction<\n TTransaction extends PrismaTransactionLike,\n> implements DBTransaction<TTransaction> {\n readonly wrappedTransaction: TTransaction;\n\n constructor(prismaTx: TTransaction) {\n this.wrappedTransaction = prismaTx;\n }\n\n runQuery<TReturn>(\n ast: AST,\n format: Format,\n schema: Schema,\n serverSchema: ServerSchema,\n ): Promise<HumanReadable<TReturn>> {\n return executePostgresQuery<TReturn>(\n this,\n ast,\n format,\n schema,\n serverSchema,\n );\n }\n\n query(sql: string, params: unknown[]): Promise<Iterable<Row>> {\n return prismaQuery(this.wrappedTransaction, sql, params);\n }\n}\n\nasync function prismaQuery(\n client: PrismaTransactionLike,\n sql: string,\n params: unknown[],\n): Promise<Iterable<Row>> {\n const result = await client.$queryRawUnsafe(sql, ...params);\n return toIterableRows(result);\n}\n\nfunction isIterable(value: unknown): value is Iterable<unknown> {\n return (\n // oxlint-disable-next-line eqeqeq\n value != null &&\n typeof (value as Iterable<unknown>)[Symbol.iterator] === 'function'\n );\n}\n\nfunction toIterableRows(result: unknown): Iterable<Row> {\n if (result === null || result === undefined) {\n return [] as Row[];\n }\n if (Array.isArray(result)) {\n return result as Row[];\n }\n if (isIterable(result)) {\n return result as Iterable<Row>;\n }\n return [] as Row[];\n}\n\n/**\n * Wrap a Prisma client for Zero ZQL.\n *\n * Provides ZQL querying plus access to the underlying Prisma transaction.\n * Use {@link PrismaTransaction} to type your server mutator transaction.\n *\n * @param schema - Zero schema.\n * @param client - Prisma client.\n *\n * @example\n * ```ts\n * import {PrismaPg} from '@prisma/adapter-pg';\n * import {PrismaClient} from '@prisma/client';\n * import {defineMutator, defineMutators} from '@rocicorp/zero';\n * import {zeroPrisma} from '@rocicorp/zero/server/adapters/prisma';\n * import {z} from 'zod/mini';\n *\n * const prisma = new PrismaClient({\n * adapter: new PrismaPg({connectionString: process.env.ZERO_UPSTREAM_DB!}),\n * });\n * const zql = zeroPrisma(schema, prisma);\n *\n * export const serverMutators = defineMutators({\n * user: {\n * create: defineMutator(\n * z.object({id: z.string(), name: z.string()}),\n * async ({tx, args}) => {\n * if (tx.location !== 'server') {\n * throw new Error('Server-only mutator');\n * }\n * await tx.dbTransaction.wrappedTransaction.user.create({\n * data: {\n * id: args.id,\n * name: args.name,\n * status: 'active',\n * },\n * });\n * },\n * ),\n * },\n * });\n * ```\n */\nexport function zeroPrisma<\n TSchema extends Schema,\n TClient extends PrismaClientLike,\n>(\n schema: TSchema,\n client: TClient,\n options?: TransactionOptions,\n): ZQLDatabase<TSchema, PrismaTransaction<TClient>> {\n return new ZQLDatabase(new PrismaConnection(client, options), schema);\n}\n"],"mappings":";;;AA8CA,IAAM,0BAAwE;CAC5E,kBAAkB;CAClB,mBAAmB;CACnB,gBAAgB;AAClB;AAcA,IAAa,mBAAb,MAEsD;CACpD;CACA;CAEA,YAAY,QAAiB,UAA8B,CAAC,GAAG;EAC7D,KAAKA,UAAU;EACf,KAAKC,WAAW;CAClB;CAEA,MAAM,KAAa,QAA2C;EAC5D,OAAO,YAAY,KAAKD,SAAS,KAAK,MAAM;CAC9C;CAEA,YACE,IACY;EACZ,MAAM,EAAC,mBAAkB,KAAKC;EAC9B,OAAO,KAAKD,QAAQ,cAClB,aACE,GACE,IAAI,0BAA0B,QAAQ,CAGxC,GACF,mBAAmB,KAAA,IACf,KAAA,IACA,EAAC,gBAAgB,wBAAwB,gBAAe,CAC9D;CACF;AACF;AAEA,IAAM,4BAAN,MAEyC;CACvC;CAEA,YAAY,UAAwB;EAClC,KAAK,qBAAqB;CAC5B;CAEA,SACE,KACA,QACA,QACA,cACiC;EACjC,OAAO,qBACL,MACA,KACA,QACA,QACA,YACF;CACF;CAEA,MAAM,KAAa,QAA2C;EAC5D,OAAO,YAAY,KAAK,oBAAoB,KAAK,MAAM;CACzD;AACF;AAEA,eAAe,YACb,QACA,KACA,QACwB;CAExB,OAAO,eAAe,MADD,OAAO,gBAAgB,KAAK,GAAG,MAAM,CAC9B;AAC9B;AAEA,SAAS,WAAW,OAA4C;CAC9D,OAEE,SAAS,QACT,OAAQ,MAA4B,OAAO,cAAc;AAE7D;AAEA,SAAS,eAAe,QAAgC;CACtD,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,OAAO,CAAC;CAEV,IAAI,MAAM,QAAQ,MAAM,GACtB,OAAO;CAET,IAAI,WAAW,MAAM,GACnB,OAAO;CAET,OAAO,CAAC;AACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,WAId,QACA,QACA,SACkD;CAClD,OAAO,IAAI,YAAY,IAAI,iBAAiB,QAAQ,OAAO,GAAG,MAAM;AACtE"}
@@ -5,6 +5,7 @@ export type { ServerColumnSchema, ServerSchema, ServerTableSchema, } from '../..
5
5
  export type { AnyTransaction, ClientTransaction, DBConnection, DBTransaction, Location, MutateCRUD, Queryable, RetryPredicate, Row, ServerTransaction, Transaction, TransactionBase, TransactionReason, } from '../../zql/src/mutate/custom.ts';
6
6
  export { CRUDMutatorFactory, makeSchemaCRUD, type CustomMutatorDefs, } from './custom.ts';
7
7
  export { executePostgresQuery } from './pg-query-executor.ts';
8
+ export type { IsolationLevel, TransactionOptions, } from './transaction-options.ts';
8
9
  export { getMutation, handleMutateRequest, handleMutationRequest, OutOfOrderMutation, type Database, type ExtractTransactionType, type MutateRequestHandler, type Params, type Parsed, type TransactFn, type TransactFnCallback, type TransactionProviderHooks, type TransactionProviderInput, } from './process-mutations.ts';
9
10
  export { PushProcessor } from './push-processor.ts';
10
11
  export { handleGetQueriesRequest, handleQueryRequest, handleTransformRequest, type QueryRequestHandler, type TransformQueryFunction, } from './queries/process-queries.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../../zero-server/src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,uBAAuB,GAC7B,MAAM,8CAA8C,CAAC;AACtD,YAAY,EAAC,cAAc,EAAC,MAAM,0CAA0C,CAAC;AAC7E,YAAY,EAAC,aAAa,EAAC,MAAM,yCAAyC,CAAC;AAC3E,YAAY,EACV,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,GAClB,MAAM,uCAAuC,CAAC;AAC/C,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,UAAU,EACV,SAAS,EACT,cAAc,EACd,GAAG,EACH,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,iBAAiB,GAClB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,MAAM,EACX,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAClD,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,GAC5B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../../../zero-server/src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,uBAAuB,GAC7B,MAAM,8CAA8C,CAAC;AACtD,YAAY,EAAC,cAAc,EAAC,MAAM,0CAA0C,CAAC;AAC7E,YAAY,EAAC,aAAa,EAAC,MAAM,yCAAyC,CAAC;AAC3E,YAAY,EACV,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,GAClB,MAAM,uCAAuC,CAAC;AAC/C,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,UAAU,EACV,SAAS,EACT,cAAc,EACd,GAAG,EACH,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,iBAAiB,GAClB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAC5D,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,MAAM,EACX,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAClD,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,GAC5B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The Postgres transaction isolation level a mutator transaction runs at.
3
+ *
4
+ * Spelled as Postgres spells it, so a level can be passed through to adapters
5
+ * that take it verbatim.
6
+ */
7
+ export type IsolationLevel = 'read committed' | 'repeatable read' | 'serializable';
8
+ /**
9
+ * Options shared by every server adapter factory.
10
+ */
11
+ export type TransactionOptions = {
12
+ /**
13
+ * Isolation level for the transaction each mutation runs in.
14
+ *
15
+ * Left unset, the transaction is opened with a bare `BEGIN` and the database
16
+ * decides, which is `read committed` on a stock Postgres.
17
+ */
18
+ readonly isolationLevel?: IsolationLevel | undefined;
19
+ };
20
+ /**
21
+ * `BEGIN`, carrying an isolation level when one was asked for.
22
+ */
23
+ export declare function beginStatement(isolationLevel: IsolationLevel | undefined): string;
24
+ //# sourceMappingURL=transaction-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-options.d.ts","sourceRoot":"","sources":["../../../../zero-server/src/transaction-options.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GACtB,gBAAgB,GAChB,iBAAiB,GACjB,cAAc,CAAC;AAEnB;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACtD,CAAC;AAEF;;GAEG;AACH,wBAAgB,cAAc,CAC5B,cAAc,EAAE,cAAc,GAAG,SAAS,GACzC,MAAM,CAIR"}
@@ -0,0 +1,11 @@
1
+ //#region ../zero-server/src/transaction-options.ts
2
+ /**
3
+ * `BEGIN`, carrying an isolation level when one was asked for.
4
+ */
5
+ function beginStatement(isolationLevel) {
6
+ return isolationLevel === void 0 ? "BEGIN" : `BEGIN ISOLATION LEVEL ${isolationLevel.toUpperCase()}`;
7
+ }
8
+ //#endregion
9
+ export { beginStatement };
10
+
11
+ //# sourceMappingURL=transaction-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-options.js","names":[],"sources":["../../../../zero-server/src/transaction-options.ts"],"sourcesContent":["/**\n * The Postgres transaction isolation level a mutator transaction runs at.\n *\n * Spelled as Postgres spells it, so a level can be passed through to adapters\n * that take it verbatim.\n */\nexport type IsolationLevel =\n | 'read committed'\n | 'repeatable read'\n | 'serializable';\n\n/**\n * Options shared by every server adapter factory.\n */\nexport type TransactionOptions = {\n /**\n * Isolation level for the transaction each mutation runs in.\n *\n * Left unset, the transaction is opened with a bare `BEGIN` and the database\n * decides, which is `read committed` on a stock Postgres.\n */\n readonly isolationLevel?: IsolationLevel | undefined;\n};\n\n/**\n * `BEGIN`, carrying an isolation level when one was asked for.\n */\nexport function beginStatement(\n isolationLevel: IsolationLevel | undefined,\n): string {\n return isolationLevel === undefined\n ? 'BEGIN'\n : `BEGIN ISOLATION LEVEL ${isolationLevel.toUpperCase()}`;\n}\n"],"mappings":";;;;AA2BA,SAAgB,eACd,gBACQ;CACR,OAAO,mBAAmB,KAAA,IACtB,UACA,yBAAyB,eAAe,YAAY;AAC1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-cost-model.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,sCAAsC,CAAC;AAEtE,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAmBjD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAC,CAAC,GAC9D,mBAAmB,CAgErB;AAqJD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C"}
1
+ {"version":3,"file":"sqlite-cost-model.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,sCAAsC,CAAC;AAEtE,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAmBjD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAC,CAAC,GAC9D,mBAAmB,CAgErB;AA8JD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C"}
@@ -91,6 +91,7 @@ function getScanstatusLoops(stmt) {
91
91
  for (let idx = 0;; idx++) {
92
92
  const selectId = stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_SELECTID, 1);
93
93
  if (selectId === void 0) break;
94
+ if (selectId === -1) continue;
94
95
  loops.push({
95
96
  selectId: must(selectId),
96
97
  parentId: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1)),
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-cost-model.js","names":[],"sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"sourcesContent":["import SQLite3Database from '@rocicorp/zero-sqlite3';\nimport {assert} from '../../shared/src/asserts.ts';\nimport {must} from '../../shared/src/must.ts';\nimport type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {SchemaValue} from '../../zero-types/src/schema-value.ts';\nimport {transformFilters} from '../../zql/src/builder/filter.ts';\nimport type {\n AccessPlan,\n ConnectionCostModel,\n CostModelCost,\n} from '../../zql/src/planner/planner-connection.ts';\nimport type {PlannerConstraint} from '../../zql/src/planner/planner-constraint.ts';\nimport type {Database, Statement} from './db.ts';\nimport {compileInline} from './internal/sql-inline.ts';\nimport {buildSelectQuery} from './query-builder.ts';\nimport {SQLiteStatFanout} from './sqlite-stat-fanout.ts';\n\n/**\n * Loop information returned by SQLite's scanstatus API.\n */\ninterface ScanstatusLoop {\n /** Unique identifier for this loop */\n selectId: number;\n /** Parent loop ID, or 0 for root loops */\n parentId: number;\n /** Estimated rows emitted per turn of parent loop */\n est: number;\n /** EXPLAIN text for this loop to determine: b-tree vs list subquery */\n explain: string;\n}\n\n/**\n * Creates a SQLite-based cost model for query planning.\n * Uses SQLite's scanstatus API to estimate query costs based on the actual\n * SQLite query planner's analysis.\n *\n * @param db Database instance for preparing statements\n * @param tableSpecs Map of table names to their table specs with ZQL schemas\n * @returns ConnectionCostModel function for use with the planner\n */\nexport function createSQLiteCostModel(\n db: Database,\n tableSpecs: Map<string, {zqlSpec: Record<string, SchemaValue>}>,\n): ConnectionCostModel {\n const fanoutEstimator = new SQLiteStatFanout(db);\n const tableRows = tableRowsReader(db);\n return (\n tableName: string,\n sort: Ordering,\n filters: Condition | undefined,\n constraint: PlannerConstraint | undefined,\n ): CostModelCost => {\n // Transform filters to remove correlated subqueries.\n //\n // We use `transformFilters` (the same function the runtime uses to\n // decide what to push to the source at connection time). It returns:\n // - For AND: the non-CSQ conjuncts (a *superset* of the original).\n // - For OR with any CSQ branch: `undefined` (the OR-with-CSQ is not\n // pushable, and matches a strict superset of any single branch).\n //\n // This matches what the runtime actually applies at the source. A\n // previous implementation here had wrong OR semantics — it dropped the\n // CSQ branch and returned the simple branch alone, which is a strict\n // *subset* of the original OR's match set, causing the cost model to\n // under-estimate scan rows for OR-with-CSQ shapes and biasing the\n // planner toward semi-join plans. See the comment in\n // `packages/zql/src/builder/filter.ts:transformFilters`.\n const noSubqueryFilters = transformFilters(filters).filters;\n\n // Build the SQL query using the same logic as actual queries\n const {zqlSpec} = must(tableSpecs.get(tableName));\n\n const query = buildSelectQuery(\n tableName,\n zqlSpec,\n constraint,\n noSubqueryFilters,\n sort,\n undefined, // reverse is undefined here\n undefined, // start is undefined here\n );\n\n // Use compileInline to inline actual values into the SQL for cost estimation.\n // This allows SQLite's query planner to see real values and make better decisions\n // about index usage and query plans. This is safe here because it's only used for\n // cost estimation, not for executing user-facing queries (which use parameterized\n // queries via the standard compile() function).\n const sql = compileInline(query);\n\n // Prepare statement to get scanstatus information\n const stmt = db.prepare(sql);\n\n // Get scanstatus loops from the prepared statement\n const loops = getScanstatusLoops(stmt);\n\n // Scanstatus should always be available - if we get no loops, something is wrong\n assert(\n loops.length > 0,\n `Expected scanstatus to return at least one loop for query: ${sql}`,\n );\n\n const ret = estimateCost(loops, (columns: string[]) =>\n fanoutEstimator.getFanout(tableName, columns),\n );\n\n return {...ret, plan: accessPlan(loops, tableRows(tableName))};\n };\n}\n\n/**\n * Returns a function that reads the number of rows in a table from\n * `sqlite_stat1`, or `undefined` if the table has not been analyzed.\n *\n * The counts are cached: they only feed plan warnings, for which the count\n * as of the first plan is close enough.\n */\nfunction tableRowsReader(db: Database): (table: string) => number | undefined {\n let stmt: Statement | undefined;\n const read = (table: string): number | undefined => {\n try {\n stmt ??= db.prepare(\n 'SELECT stat FROM sqlite_stat1 WHERE tbl = ? LIMIT 1',\n );\n // Every row for a table starts with the number of rows in the table.\n const row = stmt.get<{stat: string} | undefined>(table);\n const rows = row ? parseInt(row.stat, 10) : NaN;\n return Number.isNaN(rows) ? undefined : rows;\n } catch {\n // sqlite_stat1 does not exist until the database is analyzed.\n return undefined;\n }\n };\n // Tables without statistics are cached too, as `undefined`.\n const cache = new Map<string, number | undefined>();\n return table => {\n if (!cache.has(table)) {\n cache.set(table, read(table));\n }\n return cache.get(table);\n };\n}\n\n/**\n * Describes how SQLite reads the rows, from the EXPLAIN text of the\n * statement's top-level loops, e.g.:\n *\n * - `SEARCH issue USING INDEX issue_project (projectID=?)`\n * - `SCAN comment USING INDEX sqlite_autoindex_comment_1` (every row, in the\n * order of the index)\n * - `SCAN issue` (every row, in table order)\n * - `USE TEMP B-TREE FOR ORDER BY` (a full sort)\n * - `USE TEMP B-TREE FOR LAST TERM OF ORDER BY` or `... RIGHT PART OF ORDER\n * BY` (sorting runs of rows that tie on the index)\n */\nfunction accessPlan(\n loops: ScanstatusLoop[],\n tableRows: number | undefined,\n): AccessPlan {\n const [first, ...rest] = loops.filter(loop => loop.parentId === 0);\n let sort: AccessPlan['sort'] = 'none';\n for (const {explain} of rest) {\n if (\n explain.startsWith('USE TEMP B-TREE FOR') &&\n explain.includes('ORDER BY')\n ) {\n sort = explain === 'USE TEMP B-TREE FOR ORDER BY' ? 'full' : 'partial';\n }\n }\n return {\n access: first?.explain.startsWith('SCAN ') ? 'scan' : 'search',\n sort,\n tableRows,\n };\n}\n\n/**\n * Gets scanstatus loop information from a prepared statement.\n * Iterates through all query elements and extracts loop statistics.\n *\n * Uses SQLITE_SCANSTAT_COMPLEX flag (1) to get all loops including sorting operations.\n *\n * @param stmt Prepared statement to get scanstatus from\n * @returns Array of loop information, or empty array if scanstatus unavailable\n */\nfunction getScanstatusLoops(stmt: Statement): ScanstatusLoop[] {\n const loops: ScanstatusLoop[] = [];\n\n // Iterate through query elements by incrementing idx until we get undefined\n // which indicates we've reached the end\n for (let idx = 0; ; idx++) {\n const selectId = stmt.scanStatus(\n idx,\n SQLite3Database.SQLITE_SCANSTAT_SELECTID,\n 1,\n );\n\n if (selectId === undefined) {\n break;\n }\n\n loops.push({\n selectId: must(selectId),\n parentId: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1),\n ),\n explain: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1),\n ),\n est: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EST, 1)),\n });\n }\n\n return loops.sort((a, b) => a.selectId - b.selectId);\n}\n\n/**\n * Estimates the cost of a query based on scanstats from sqlite3_stmt_scanstatus_v2\n */\nfunction estimateCost(\n scanstats: ScanstatusLoop[],\n fanout: CostModelCost['fanout'],\n): CostModelCost {\n // Sort by selectId to process in execution order\n const sorted = scanstats.toSorted((a, b) => a.selectId - b.selectId);\n\n let totalRows = 0;\n let totalCost = 0;\n\n // Identify if there are multiple top-level (parentId=0) operations\n // If so, the first is typically the scan, and subsequent ones are sorts\n const topLevelOps = sorted.filter(s => s.parentId === 0);\n\n // We only consider top level ops since ZQL queries are single-table when hitting SQLite.\n // We do have a nested op in the case of `WHERE x IN (:arg)` but it is negligible\n // assuming :arg is small.\n let firstLoop = true;\n for (const op of topLevelOps) {\n if (firstLoop) {\n // First top-level op is the main scan\n // and determines the total number of rows output.\n totalRows = op.est;\n firstLoop = false;\n } else {\n if (op.explain.includes('ORDER BY')) {\n totalCost += btreeCost(totalRows);\n }\n }\n }\n\n return {\n rows: totalRows,\n startupCost: totalCost,\n fanout,\n };\n}\n\nexport function btreeCost(rows: number): number {\n // B-Tree construction is ~O(n log n) so we estimate the cost as such.\n // We divide the cost by 10 because sorting in SQLite is ~10x faster\n // than bringing the data into JS and sorting there.\n return (rows * Math.log2(rows)) / 10;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqlite-cost-model.js","names":[],"sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"sourcesContent":["import SQLite3Database from '@rocicorp/zero-sqlite3';\nimport {assert} from '../../shared/src/asserts.ts';\nimport {must} from '../../shared/src/must.ts';\nimport type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {SchemaValue} from '../../zero-types/src/schema-value.ts';\nimport {transformFilters} from '../../zql/src/builder/filter.ts';\nimport type {\n AccessPlan,\n ConnectionCostModel,\n CostModelCost,\n} from '../../zql/src/planner/planner-connection.ts';\nimport type {PlannerConstraint} from '../../zql/src/planner/planner-constraint.ts';\nimport type {Database, Statement} from './db.ts';\nimport {compileInline} from './internal/sql-inline.ts';\nimport {buildSelectQuery} from './query-builder.ts';\nimport {SQLiteStatFanout} from './sqlite-stat-fanout.ts';\n\n/**\n * Loop information returned by SQLite's scanstatus API.\n */\ninterface ScanstatusLoop {\n /** Unique identifier for this loop */\n selectId: number;\n /** Parent loop ID, or 0 for root loops */\n parentId: number;\n /** Estimated rows emitted per turn of parent loop */\n est: number;\n /** EXPLAIN text for this loop to determine: b-tree vs list subquery */\n explain: string;\n}\n\n/**\n * Creates a SQLite-based cost model for query planning.\n * Uses SQLite's scanstatus API to estimate query costs based on the actual\n * SQLite query planner's analysis.\n *\n * @param db Database instance for preparing statements\n * @param tableSpecs Map of table names to their table specs with ZQL schemas\n * @returns ConnectionCostModel function for use with the planner\n */\nexport function createSQLiteCostModel(\n db: Database,\n tableSpecs: Map<string, {zqlSpec: Record<string, SchemaValue>}>,\n): ConnectionCostModel {\n const fanoutEstimator = new SQLiteStatFanout(db);\n const tableRows = tableRowsReader(db);\n return (\n tableName: string,\n sort: Ordering,\n filters: Condition | undefined,\n constraint: PlannerConstraint | undefined,\n ): CostModelCost => {\n // Transform filters to remove correlated subqueries.\n //\n // We use `transformFilters` (the same function the runtime uses to\n // decide what to push to the source at connection time). It returns:\n // - For AND: the non-CSQ conjuncts (a *superset* of the original).\n // - For OR with any CSQ branch: `undefined` (the OR-with-CSQ is not\n // pushable, and matches a strict superset of any single branch).\n //\n // This matches what the runtime actually applies at the source. A\n // previous implementation here had wrong OR semantics — it dropped the\n // CSQ branch and returned the simple branch alone, which is a strict\n // *subset* of the original OR's match set, causing the cost model to\n // under-estimate scan rows for OR-with-CSQ shapes and biasing the\n // planner toward semi-join plans. See the comment in\n // `packages/zql/src/builder/filter.ts:transformFilters`.\n const noSubqueryFilters = transformFilters(filters).filters;\n\n // Build the SQL query using the same logic as actual queries\n const {zqlSpec} = must(tableSpecs.get(tableName));\n\n const query = buildSelectQuery(\n tableName,\n zqlSpec,\n constraint,\n noSubqueryFilters,\n sort,\n undefined, // reverse is undefined here\n undefined, // start is undefined here\n );\n\n // Use compileInline to inline actual values into the SQL for cost estimation.\n // This allows SQLite's query planner to see real values and make better decisions\n // about index usage and query plans. This is safe here because it's only used for\n // cost estimation, not for executing user-facing queries (which use parameterized\n // queries via the standard compile() function).\n const sql = compileInline(query);\n\n // Prepare statement to get scanstatus information\n const stmt = db.prepare(sql);\n\n // Get scanstatus loops from the prepared statement\n const loops = getScanstatusLoops(stmt);\n\n // Scanstatus should always be available - if we get no loops, something is wrong\n assert(\n loops.length > 0,\n `Expected scanstatus to return at least one loop for query: ${sql}`,\n );\n\n const ret = estimateCost(loops, (columns: string[]) =>\n fanoutEstimator.getFanout(tableName, columns),\n );\n\n return {...ret, plan: accessPlan(loops, tableRows(tableName))};\n };\n}\n\n/**\n * Returns a function that reads the number of rows in a table from\n * `sqlite_stat1`, or `undefined` if the table has not been analyzed.\n *\n * The counts are cached: they only feed plan warnings, for which the count\n * as of the first plan is close enough.\n */\nfunction tableRowsReader(db: Database): (table: string) => number | undefined {\n let stmt: Statement | undefined;\n const read = (table: string): number | undefined => {\n try {\n stmt ??= db.prepare(\n 'SELECT stat FROM sqlite_stat1 WHERE tbl = ? LIMIT 1',\n );\n // Every row for a table starts with the number of rows in the table.\n const row = stmt.get<{stat: string} | undefined>(table);\n const rows = row ? parseInt(row.stat, 10) : NaN;\n return Number.isNaN(rows) ? undefined : rows;\n } catch {\n // sqlite_stat1 does not exist until the database is analyzed.\n return undefined;\n }\n };\n // Tables without statistics are cached too, as `undefined`.\n const cache = new Map<string, number | undefined>();\n return table => {\n if (!cache.has(table)) {\n cache.set(table, read(table));\n }\n return cache.get(table);\n };\n}\n\n/**\n * Describes how SQLite reads the rows, from the EXPLAIN text of the\n * statement's top-level loops, e.g.:\n *\n * - `SEARCH issue USING INDEX issue_project (projectID=?)`\n * - `SCAN comment USING INDEX sqlite_autoindex_comment_1` (every row, in the\n * order of the index)\n * - `SCAN issue` (every row, in table order)\n * - `USE TEMP B-TREE FOR ORDER BY` (a full sort)\n * - `USE TEMP B-TREE FOR LAST TERM OF ORDER BY` or `... RIGHT PART OF ORDER\n * BY` (sorting runs of rows that tie on the index)\n */\nfunction accessPlan(\n loops: ScanstatusLoop[],\n tableRows: number | undefined,\n): AccessPlan {\n const [first, ...rest] = loops.filter(loop => loop.parentId === 0);\n let sort: AccessPlan['sort'] = 'none';\n for (const {explain} of rest) {\n if (\n explain.startsWith('USE TEMP B-TREE FOR') &&\n explain.includes('ORDER BY')\n ) {\n sort = explain === 'USE TEMP B-TREE FOR ORDER BY' ? 'full' : 'partial';\n }\n }\n return {\n access: first?.explain.startsWith('SCAN ') ? 'scan' : 'search',\n sort,\n tableRows,\n };\n}\n\n/**\n * Gets scanstatus loop information from a prepared statement.\n * Iterates through all query elements and extracts loop statistics.\n *\n * Uses SQLITE_SCANSTAT_COMPLEX flag (1) to get all loops including sorting operations.\n *\n * @param stmt Prepared statement to get scanstatus from\n * @returns Array of loop information, or empty array if scanstatus unavailable\n */\nfunction getScanstatusLoops(stmt: Statement): ScanstatusLoop[] {\n const loops: ScanstatusLoop[] = [];\n\n // Iterate through query elements by incrementing idx until we get undefined\n // which indicates we've reached the end\n for (let idx = 0; ; idx++) {\n const selectId = stmt.scanStatus(\n idx,\n SQLite3Database.SQLITE_SCANSTAT_SELECTID,\n 1,\n );\n\n if (selectId === undefined) {\n break;\n }\n\n // A MULTI-INDEX OR nested in a branch of another one gets a scanstatus\n // entry without an OP_Explain: SQLite reports its SELECTID and PARENTID\n // as -1 and its EXPLAIN as NULL. The nested OR's own `MULTI-INDEX OR` and\n // `INDEX n` loops are reported separately, and the entry is never a\n // top-level loop, so nothing the estimate reads is lost by skipping it.\n if (selectId === -1) {\n continue;\n }\n\n loops.push({\n selectId: must(selectId),\n parentId: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_PARENTID, 1),\n ),\n explain: must(\n stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EXPLAIN, 1),\n ),\n est: must(stmt.scanStatus(idx, SQLite3Database.SQLITE_SCANSTAT_EST, 1)),\n });\n }\n\n return loops.sort((a, b) => a.selectId - b.selectId);\n}\n\n/**\n * Estimates the cost of a query based on scanstats from sqlite3_stmt_scanstatus_v2\n */\nfunction estimateCost(\n scanstats: ScanstatusLoop[],\n fanout: CostModelCost['fanout'],\n): CostModelCost {\n // Sort by selectId to process in execution order\n const sorted = scanstats.toSorted((a, b) => a.selectId - b.selectId);\n\n let totalRows = 0;\n let totalCost = 0;\n\n // Identify if there are multiple top-level (parentId=0) operations\n // If so, the first is typically the scan, and subsequent ones are sorts\n const topLevelOps = sorted.filter(s => s.parentId === 0);\n\n // We only consider top level ops since ZQL queries are single-table when hitting SQLite.\n // We do have a nested op in the case of `WHERE x IN (:arg)` but it is negligible\n // assuming :arg is small.\n let firstLoop = true;\n for (const op of topLevelOps) {\n if (firstLoop) {\n // First top-level op is the main scan\n // and determines the total number of rows output.\n totalRows = op.est;\n firstLoop = false;\n } else {\n if (op.explain.includes('ORDER BY')) {\n totalCost += btreeCost(totalRows);\n }\n }\n }\n\n return {\n rows: totalRows,\n startupCost: totalCost,\n fanout,\n };\n}\n\nexport function btreeCost(rows: number): number {\n // B-Tree construction is ~O(n log n) so we estimate the cost as such.\n // We divide the cost by 10 because sorting in SQLite is ~10x faster\n // than bringing the data into JS and sorting there.\n return (rows * Math.log2(rows)) / 10;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocicorp/zero",
3
- "version": "1.11.0-canary.38",
3
+ "version": "1.11.0-canary.39",
4
4
  "description": "Zero is a web framework for serverless web development.",
5
5
  "homepage": "https://zero.rocicorp.dev",
6
6
  "bugs": {