@rocicorp/zero 1.2.0 → 1.3.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/analyze-query/src/bin-analyze.js +25 -25
- package/out/analyze-query/src/bin-analyze.js.map +1 -1
- package/out/ast-to-zql/src/ast-to-zql.d.ts.map +1 -1
- package/out/ast-to-zql/src/ast-to-zql.js +2 -1
- package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
- package/out/replicache/src/btree/node.d.ts.map +1 -1
- package/out/replicache/src/btree/node.js +2 -2
- package/out/replicache/src/btree/node.js.map +1 -1
- package/out/replicache/src/connection-loop.js +3 -3
- package/out/replicache/src/connection-loop.js.map +1 -1
- package/out/replicache/src/deleted-clients.d.ts +0 -4
- package/out/replicache/src/deleted-clients.d.ts.map +1 -1
- package/out/replicache/src/deleted-clients.js +1 -1
- package/out/replicache/src/deleted-clients.js.map +1 -1
- package/out/replicache/src/hash.d.ts.map +1 -1
- package/out/replicache/src/hash.js.map +1 -1
- package/out/replicache/src/process-scheduler.d.ts.map +1 -1
- package/out/replicache/src/process-scheduler.js.map +1 -1
- package/out/replicache/src/request-idle.js +1 -1
- package/out/replicache/src/request-idle.js.map +1 -1
- package/out/replicache/src/sync/patch.d.ts +1 -1
- package/out/replicache/src/sync/patch.d.ts.map +1 -1
- package/out/replicache/src/sync/patch.js +1 -1
- package/out/replicache/src/sync/patch.js.map +1 -1
- package/out/shared/src/arrays.d.ts.map +1 -1
- package/out/shared/src/arrays.js +1 -2
- package/out/shared/src/arrays.js.map +1 -1
- package/out/shared/src/bigint-json.js +1 -1
- package/out/shared/src/bigint-json.js.map +1 -1
- package/out/shared/src/btree-set.js +1 -1
- package/out/shared/src/btree-set.js.map +1 -1
- package/out/shared/src/iterables.d.ts +7 -0
- package/out/shared/src/iterables.d.ts.map +1 -1
- package/out/shared/src/iterables.js +10 -1
- package/out/shared/src/iterables.js.map +1 -1
- package/out/shared/src/logging.d.ts.map +1 -1
- package/out/shared/src/logging.js +10 -9
- package/out/shared/src/logging.js.map +1 -1
- package/out/shared/src/options.js +1 -1
- package/out/shared/src/options.js.map +1 -1
- package/out/shared/src/sorted-entries.d.ts +2 -0
- package/out/shared/src/sorted-entries.d.ts.map +1 -0
- package/out/shared/src/sorted-entries.js +9 -0
- package/out/shared/src/sorted-entries.js.map +1 -0
- package/out/shared/src/tdigest-schema.d.ts.map +1 -1
- package/out/shared/src/tdigest-schema.js.map +1 -1
- package/out/shared/src/tdigest.d.ts.map +1 -1
- package/out/shared/src/tdigest.js +7 -7
- package/out/shared/src/tdigest.js.map +1 -1
- package/out/shared/src/valita.d.ts.map +1 -1
- package/out/shared/src/valita.js +1 -1
- package/out/shared/src/valita.js.map +1 -1
- package/out/z2s/src/sql.d.ts +2 -2
- package/out/z2s/src/sql.d.ts.map +1 -1
- package/out/z2s/src/sql.js +3 -3
- package/out/z2s/src/sql.js.map +1 -1
- package/out/zero/package.js +6 -7
- package/out/zero/package.js.map +1 -1
- package/out/zero/src/pg.js +1 -1
- package/out/zero/src/server.js +1 -1
- package/out/zero-cache/src/auth/auth.d.ts +8 -26
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js +57 -82
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/auth/jwt.d.ts +3 -3
- package/out/zero-cache/src/auth/jwt.d.ts.map +1 -1
- package/out/zero-cache/src/auth/jwt.js.map +1 -1
- package/out/zero-cache/src/auth/load-permissions.js +1 -1
- package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +38 -2
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +56 -1
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom/fetch.d.ts +2 -9
- package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
- package/out/zero-cache/src/custom/fetch.js +11 -4
- package/out/zero-cache/src/custom/fetch.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.d.ts +20 -9
- package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +74 -37
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +1 -1
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/migration.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration.js +1 -1
- package/out/zero-cache/src/db/migration.js.map +1 -1
- package/out/zero-cache/src/db/pg-copy-binary.d.ts +101 -0
- package/out/zero-cache/src/db/pg-copy-binary.d.ts.map +1 -0
- package/out/zero-cache/src/db/pg-copy-binary.js +381 -0
- package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -0
- package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +3 -0
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/db/warmup.d.ts.map +1 -1
- package/out/zero-cache/src/db/warmup.js +3 -1
- package/out/zero-cache/src/db/warmup.js.map +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.d.ts.map +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js +2 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +5 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.d.ts +2 -2
- package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js +4 -4
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/server/main.js +1 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/reaper.d.ts.map +1 -1
- package/out/zero-cache/src/server/reaper.js +4 -1
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +41 -20
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-urls.d.ts.map +1 -1
- package/out/zero-cache/src/server/worker-urls.js +2 -1
- package/out/zero-cache/src/server/worker-urls.js.map +1 -1
- package/out/zero-cache/src/services/change-source/change-source.d.ts +4 -0
- package/out/zero-cache/src/services/change-source/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js +3 -2
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +5 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +13 -4
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +3 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +91 -9
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +3 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +5 -4
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +11 -11
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +5 -5
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.d.ts +20 -20
- package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +91 -104
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-schema.js +4 -3
- package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +168 -0
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +385 -0
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +2 -2
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +5 -4
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +2 -3
- package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +3 -3
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +5 -3
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +13 -7
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +3 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +6 -9
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +24 -26
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +236 -124
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/lite.d.ts.map +1 -1
- package/out/zero-cache/src/types/lite.js +3 -2
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg-types.js +4 -1
- package/out/zero-cache/src/types/pg-types.js.map +1 -1
- package/out/zero-cache/src/types/pg-versions.d.ts +3 -0
- package/out/zero-cache/src/types/pg-versions.d.ts.map +1 -0
- package/out/zero-cache/src/types/pg-versions.js +7 -0
- package/out/zero-cache/src/types/pg-versions.js.map +1 -0
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js +6 -1
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
- package/out/zero-cache/src/types/subscription.js +2 -2
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/workers/connect-params.d.ts +1 -1
- package/out/zero-cache/src/workers/connect-params.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connect-params.js +1 -1
- package/out/zero-cache/src/workers/connect-params.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +2 -2
- package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts +2 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +64 -38
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts +2 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +70 -31
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/connection.d.ts +4 -4
- package/out/zero-client/src/client/connection.d.ts.map +1 -1
- package/out/zero-client/src/client/connection.js.map +1 -1
- package/out/zero-client/src/client/http-string.d.ts.map +1 -1
- package/out/zero-client/src/client/http-string.js.map +1 -1
- package/out/zero-client/src/client/metrics.d.ts.map +1 -1
- package/out/zero-client/src/client/metrics.js +2 -1
- package/out/zero-client/src/client/metrics.js.map +1 -1
- package/out/zero-client/src/client/options.d.ts +34 -5
- package/out/zero-client/src/client/options.d.ts.map +1 -1
- package/out/zero-client/src/client/options.js.map +1 -1
- package/out/zero-client/src/client/server-option.js +1 -1
- package/out/zero-client/src/client/server-option.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.d.ts.map +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
- package/out/zero-client/src/client/zero.d.ts +4 -3
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +33 -11
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-pg/src/mod.js +1 -1
- package/out/zero-protocol/src/ast.d.ts.map +1 -1
- package/out/zero-protocol/src/ast.js.map +1 -1
- package/out/zero-protocol/src/change-desired-queries.d.ts +4 -0
- package/out/zero-protocol/src/change-desired-queries.d.ts.map +1 -1
- package/out/zero-protocol/src/change-desired-queries.js +4 -1
- package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
- package/out/zero-protocol/src/connect.d.ts +4 -0
- package/out/zero-protocol/src/connect.d.ts.map +1 -1
- package/out/zero-protocol/src/connect.js +2 -1
- package/out/zero-protocol/src/connect.js.map +1 -1
- package/out/zero-protocol/src/primary-key.d.ts.map +1 -1
- package/out/zero-protocol/src/primary-key.js.map +1 -1
- package/out/zero-protocol/src/protocol-version.d.ts +1 -1
- package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
- package/out/zero-protocol/src/protocol-version.js.map +1 -1
- package/out/zero-protocol/src/push.d.ts +4 -0
- package/out/zero-protocol/src/push.d.ts.map +1 -1
- package/out/zero-protocol/src/push.js +2 -1
- package/out/zero-protocol/src/push.js.map +1 -1
- package/out/zero-protocol/src/up.d.ts +3 -0
- package/out/zero-protocol/src/up.d.ts.map +1 -1
- package/out/zero-react/src/zero-provider.d.ts.map +1 -1
- package/out/zero-react/src/zero-provider.js +11 -5
- package/out/zero-react/src/zero-provider.js.map +1 -1
- package/out/zero-schema/src/name-mapper.js +1 -1
- package/out/zero-schema/src/name-mapper.js.map +1 -1
- package/out/zero-server/src/mod.js +1 -1
- package/out/zero-server/src/process-mutations.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.js +2 -1
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zero-server/src/push-processor.d.ts +1 -0
- package/out/zero-server/src/push-processor.d.ts.map +1 -1
- package/out/zero-server/src/push-processor.js +3 -2
- package/out/zero-server/src/push-processor.js.map +1 -1
- package/out/zero-solid/src/use-zero.d.ts.map +1 -1
- package/out/zero-solid/src/use-zero.js +8 -9
- package/out/zero-solid/src/use-zero.js.map +1 -1
- package/out/zql/src/builder/like.js +2 -1
- package/out/zql/src/builder/like.js.map +1 -1
- package/out/zql/src/ivm/data.d.ts.map +1 -1
- package/out/zql/src/ivm/data.js +6 -15
- package/out/zql/src/ivm/data.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +14 -8
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/query/complete-ordering.js +1 -1
- package/out/zql/src/query/complete-ordering.js.map +1 -1
- package/out/zql/src/query/query-impl.d.ts.map +1 -1
- package/out/zql/src/query/query-impl.js +2 -2
- package/out/zql/src/query/query-impl.js.map +1 -1
- package/out/zql/src/query/query-registry.d.ts.map +1 -1
- package/out/zql/src/query/query-registry.js +2 -1
- package/out/zql/src/query/query-registry.js.map +1 -1
- package/out/zql/src/query/ttl.js +1 -1
- package/out/zql/src/query/ttl.js.map +1 -1
- package/out/zqlite/src/internal/sql.d.ts +2 -2
- package/out/zqlite/src/internal/sql.d.ts.map +1 -1
- package/out/zqlite/src/internal/sql.js +1 -2
- package/out/zqlite/src/internal/sql.js.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.d.ts +1 -1
- package/out/zqlite/src/sqlite-cost-model.d.ts.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.js +1 -1
- package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
- package/out/zqlite/src/sqlite-stat-fanout.js +1 -1
- package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +8 -12
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +6 -7
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { SQLQuery } from '@databases/sql';
|
|
2
|
-
import
|
|
2
|
+
import sql from '@databases/sql';
|
|
3
3
|
export declare function compile(sql: SQLQuery): string;
|
|
4
4
|
export declare function format(sql: SQLQuery): {
|
|
5
5
|
text: string;
|
|
6
6
|
values: unknown[];
|
|
7
7
|
};
|
|
8
|
-
export
|
|
8
|
+
export { sql };
|
|
9
9
|
//# sourceMappingURL=sql.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../../../zqlite/src/internal/sql.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../../../zqlite/src/internal/sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AAC3D,OAAO,GAAG,MAAM,gBAAgB,CAAC;AAOjC,wBAAgB,OAAO,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAE7C;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,QAAQ;;;EAEnC;AAED,OAAO,EAAC,GAAG,EAAC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeSQLiteIdentifier } from "@databases/escape-identifier";
|
|
2
|
-
import
|
|
2
|
+
import sql from "@databases/sql";
|
|
3
3
|
//#region ../zqlite/src/internal/sql.ts
|
|
4
4
|
var sqliteFormat = {
|
|
5
5
|
escapeIdentifier: (str) => escapeSQLiteIdentifier(str),
|
|
@@ -14,7 +14,6 @@ function compile(sql) {
|
|
|
14
14
|
function format(sql) {
|
|
15
15
|
return sql.format(sqliteFormat);
|
|
16
16
|
}
|
|
17
|
-
var sql = baseSql.default;
|
|
18
17
|
//#endregion
|
|
19
18
|
export { compile, format, sql };
|
|
20
19
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql.js","names":[],"sources":["../../../../../zqlite/src/internal/sql.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"sql.js","names":[],"sources":["../../../../../zqlite/src/internal/sql.ts"],"sourcesContent":["import {escapeSQLiteIdentifier} from '@databases/escape-identifier';\nimport type {FormatConfig, SQLQuery} from '@databases/sql';\nimport sql from '@databases/sql';\n\nconst sqliteFormat: FormatConfig = {\n escapeIdentifier: str => escapeSQLiteIdentifier(str),\n formatValue: value => ({placeholder: '?', value}),\n};\n\nexport function compile(sql: SQLQuery): string {\n return sql.format(sqliteFormat).text;\n}\n\nexport function format(sql: SQLQuery) {\n return sql.format(sqliteFormat);\n}\n\nexport {sql};\n"],"mappings":";;;AAIA,IAAM,eAA6B;CACjC,mBAAkB,QAAO,uBAAuB,IAAI;CACpD,cAAa,WAAU;EAAC,aAAa;EAAK;EAAM;CACjD;AAED,SAAgB,QAAQ,KAAuB;AAC7C,QAAO,IAAI,OAAO,aAAa,CAAC;;AAGlC,SAAgB,OAAO,KAAe;AACpC,QAAO,IAAI,OAAO,aAAa"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { SchemaValue } from '../../zero-types/src/schema-value.ts';
|
|
1
2
|
import type { ConnectionCostModel } from '../../zql/src/planner/planner-connection.ts';
|
|
2
3
|
import type { Database } from './db.ts';
|
|
3
|
-
import type { SchemaValue } from '../../zero-types/src/schema-value.ts';
|
|
4
4
|
/**
|
|
5
5
|
* Creates a SQLite-based cost model for query planning.
|
|
6
6
|
* Uses SQLite's scanstatus API to estimate query costs based on the actual
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-cost-model.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"names":[],"mappings":"
|
|
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;AACtE,OAAO,KAAK,EACV,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,CAqDrB;AAoHD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9C"}
|
|
@@ -81,7 +81,7 @@ function getScanstatusLoops(stmt) {
|
|
|
81
81
|
* Estimates the cost of a query based on scanstats from sqlite3_stmt_scanstatus_v2
|
|
82
82
|
*/
|
|
83
83
|
function estimateCost(scanstats, fanout) {
|
|
84
|
-
const sorted =
|
|
84
|
+
const sorted = scanstats.toSorted((a, b) => a.selectId - b.selectId);
|
|
85
85
|
let totalRows = 0;
|
|
86
86
|
let totalCost = 0;
|
|
87
87
|
const topLevelOps = sorted.filter((s) => s.parentId === 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-cost-model.js","names":[],"sources":["../../../../zqlite/src/sqlite-cost-model.ts"],"sourcesContent":["import type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {\n ConnectionCostModel,\n CostModelCost,\n} from '../../zql/src/planner/planner-connection.ts';\nimport type {PlannerConstraint} from '../../zql/src/planner/planner-constraint.ts';\nimport
|
|
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 type {\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, type NoSubqueryCondition} 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 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 // The cost model can't handle correlated subqueries, so we estimate cost\n // without them. This is conservative - actual cost may be higher.\n const noSubqueryFilters = filters\n ? removeCorrelatedSubqueries(filters)\n : undefined;\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;\n };\n}\n\n/**\n * Removes correlated subqueries from conditions.\n * The cost model estimates cost without correlated subqueries since\n * they can't be included in the scanstatus query.\n */\nfunction removeCorrelatedSubqueries(\n condition: Condition,\n): NoSubqueryCondition | undefined {\n switch (condition.type) {\n case 'correlatedSubquery':\n // Remove subqueries - we can't estimate their cost via scanstatus\n return undefined;\n case 'simple':\n return condition;\n case 'and': {\n const filtered = condition.conditions\n .map(c => removeCorrelatedSubqueries(c))\n .filter((c): c is NoSubqueryCondition => c !== undefined);\n if (filtered.length === 0) return undefined;\n if (filtered.length === 1) return filtered[0];\n return {type: 'and', conditions: filtered};\n }\n case 'or': {\n const filtered = condition.conditions\n .map(c => removeCorrelatedSubqueries(c))\n .filter((c): c is NoSubqueryCondition => c !== undefined);\n if (filtered.length === 0) return undefined;\n if (filtered.length === 1) return filtered[0];\n return {type: 'or', conditions: filtered};\n }\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":";;;;;;;;;;;;;;;;AAsCA,SAAgB,sBACd,IACA,YACqB;CACrB,MAAM,kBAAkB,IAAI,iBAAiB,GAAG;AAChD,SACE,WACA,MACA,SACA,eACkB;EAIlB,MAAM,oBAAoB,UACtB,2BAA2B,QAAQ,GACnC,KAAA;EAGJ,MAAM,EAAC,YAAW,KAAK,WAAW,IAAI,UAAU,CAAC;EAiBjD,MAAM,MAAM,cAfE,iBACZ,WACA,SACA,YACA,mBACA,MACA,KAAA,GACA,KAAA,EACD,CAO+B;EAMhC,MAAM,QAAQ,mBAHD,GAAG,QAAQ,IAAI,CAGU;AAGtC,SACE,MAAM,SAAS,GACf,8DAA8D,MAC/D;AAMD,SAJY,aAAa,QAAQ,YAC/B,gBAAgB,UAAU,WAAW,QAAQ,CAC9C;;;;;;;;AAWL,SAAS,2BACP,WACiC;AACjC,SAAQ,UAAU,MAAlB;EACE,KAAK,qBAEH;EACF,KAAK,SACH,QAAO;EACT,KAAK,OAAO;GACV,MAAM,WAAW,UAAU,WACxB,KAAI,MAAK,2BAA2B,EAAE,CAAC,CACvC,QAAQ,MAAgC,MAAM,KAAA,EAAU;AAC3D,OAAI,SAAS,WAAW,EAAG,QAAO,KAAA;AAClC,OAAI,SAAS,WAAW,EAAG,QAAO,SAAS;AAC3C,UAAO;IAAC,MAAM;IAAO,YAAY;IAAS;;EAE5C,KAAK,MAAM;GACT,MAAM,WAAW,UAAU,WACxB,KAAI,MAAK,2BAA2B,EAAE,CAAC,CACvC,QAAQ,MAAgC,MAAM,KAAA,EAAU;AAC3D,OAAI,SAAS,WAAW,EAAG,QAAO,KAAA;AAClC,OAAI,SAAS,WAAW,EAAG,QAAO,SAAS;AAC3C,UAAO;IAAC,MAAM;IAAM,YAAY;IAAS;;;;;;;;;;;;;AAc/C,SAAS,mBAAmB,MAAmC;CAC7D,MAAM,QAA0B,EAAE;AAIlC,MAAK,IAAI,MAAM,IAAK,OAAO;EACzB,MAAM,WAAW,KAAK,WACpB,KACA,gBAAgB,0BAChB,EACD;AAED,MAAI,aAAa,KAAA,EACf;AAGF,QAAM,KAAK;GACT,UAAU,KAAK,SAAS;GACxB,UAAU,KACR,KAAK,WAAW,KAAK,gBAAgB,0BAA0B,EAAE,CAClE;GACD,SAAS,KACP,KAAK,WAAW,KAAK,gBAAgB,yBAAyB,EAAE,CACjE;GACD,KAAK,KAAK,KAAK,WAAW,KAAK,gBAAgB,qBAAqB,EAAE,CAAC;GACxE,CAAC;;AAGJ,QAAO,MAAM,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS;;;;;AAMtD,SAAS,aACP,WACA,QACe;CAEf,MAAM,SAAS,UAAU,UAAU,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS;CAEpE,IAAI,YAAY;CAChB,IAAI,YAAY;CAIhB,MAAM,cAAc,OAAO,QAAO,MAAK,EAAE,aAAa,EAAE;CAKxD,IAAI,YAAY;AAChB,MAAK,MAAM,MAAM,YACf,KAAI,WAAW;AAGb,cAAY,GAAG;AACf,cAAY;YAER,GAAG,QAAQ,SAAS,WAAW,CACjC,cAAa,UAAU,UAAU;AAKvC,QAAO;EACL,MAAM;EACN,aAAa;EACb;EACD;;AAGH,SAAgB,UAAU,MAAsB;AAI9C,QAAQ,OAAO,KAAK,KAAK,KAAK,GAAI"}
|
|
@@ -127,7 +127,7 @@ var SQLiteStatFanout = class {
|
|
|
127
127
|
* @returns Fanout result with value and source
|
|
128
128
|
*/
|
|
129
129
|
getFanout(tableName, columns) {
|
|
130
|
-
const cacheKey = `${tableName}:${
|
|
130
|
+
const cacheKey = `${tableName}:${columns.toSorted().join(",")}`;
|
|
131
131
|
const cached = this.#cache.get(cacheKey);
|
|
132
132
|
if (cached) return cached;
|
|
133
133
|
const stat4Result = this.#getFanoutFromStat4(tableName, columns);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-stat-fanout.js","names":["#db","#defaultFanout","#cache","#stat4Stmt","#stat1Stmt","#indexStmt","#getFanoutFromStat4","#getFanoutFromStat1","#findIndexForColumns","#decodeSampleIsNull","#isPrefixMatch"],"sources":["../../../../zqlite/src/sqlite-stat-fanout.ts"],"sourcesContent":["import type {Database} from './db.ts';\n\n/**\n * Result of fanout calculation from SQLite statistics.\n */\nexport interface FanoutResult {\n /**\n * The fanout value (average rows per distinct value of the join column).\n * For non-NULL joins, this represents how many child rows exist per parent key.\n */\n fanout: number;\n confidence: 'high' | 'med' | 'none';\n\n /**\n * Source of the fanout calculation.\n * - 'stat4': From sqlite_stat4 histogram (most accurate, excludes NULLs)\n * - 'stat1': From sqlite_stat1 average (includes NULLs, may overestimate)\n * - 'default': Fallback constant when statistics unavailable\n */\n source: 'stat4' | 'stat1' | 'default';\n}\n\n/**\n * Sample from sqlite_stat4 histogram.\n */\ninterface Stat4Sample {\n /** \"N1 N2\" = rows equal to sample (N1=first col, N2=if composite) */\n neq: string;\n /** \"N1 N2\" = rows less than sample */\n nlt: string;\n /** \"N1 N2\" = distinct values less than sample */\n ndlt: string;\n /** The actual sample value (binary encoded) */\n sample: Buffer;\n}\n\n/**\n * Computes join fanout factors from SQLite statistics tables.\n *\n * Fanout is the average number of child rows per distinct parent key value,\n * used to estimate join cardinality in query planning.\n *\n * ## Problem\n *\n * sqlite_stat1 includes NULL rows in its calculation, which can significantly\n * overestimate fanout for sparse foreign keys:\n *\n * ```\n * Example: 100 tasks, 20 with project_id, 80 with NULL\n * - stat1 reports: \"100 17\" → fanout = 17 (WRONG - includes NULLs)\n * - stat4 shows: NULL samples with fanout=80, non-NULL samples with fanout=4\n * - True fanout: 4 (CORRECT)\n * ```\n *\n * ## Solution\n *\n * This class uses sqlite_stat4 histogram to separate NULL and non-NULL samples,\n * providing accurate fanout for non-NULL joins.\n *\n * ## Usage\n *\n * ```typescript\n * const calculator = new SQLiteStatFanout(db);\n *\n * // Get fanout for posts.userId → users.id join\n * const result = calculator.getFanout('posts', 'userId');\n *\n * if (result.source === 'stat4') {\n * // Accurate: excludes NULLs, samples actual distribution\n * console.log(`Fanout: ${result.fanout} (from stat4)`);\n * } else if (result.source === 'stat1') {\n * // Conservative: includes NULLs, may overestimate\n * console.log(`Fanout: ${result.fanout} (from stat1, includes NULLs)`);\n * } else {\n * // Fallback: no statistics available\n * console.log(`Fanout: ${result.fanout} (default estimate)`);\n * }\n * ```\n *\n * ## Requirements\n *\n * - SQLite compiled with ENABLE_STAT4 (most builds include this)\n * - `ANALYZE` command run on the database\n * - Index exists on the join column\n *\n * @see https://sqlite.org/fileformat2.html#stat4tab\n * @see packages/zql/src/planner/SELECTIVITY_PLAN.md\n */\nexport class SQLiteStatFanout {\n readonly #db: Database;\n readonly #defaultFanout: number;\n\n /**\n * Cache of fanout results by table and columns.\n * Key format: \"tableName:col1,col2,col3\" (sorted alphabetically)\n */\n readonly #cache = new Map<string, FanoutResult>();\n\n /**\n * Prepared statements for querying SQLite statistics tables.\n * Prepared once in constructor for performance.\n */\n readonly #stat4Stmt: ReturnType<Database['prepare']>;\n readonly #stat1Stmt: ReturnType<Database['prepare']>;\n readonly #indexStmt: ReturnType<Database['prepare']>;\n\n /**\n * Creates a new fanout calculator.\n *\n * @param db Database instance\n * @param defaultFanout Default fanout when statistics unavailable (default: 3)\n * - 1: Conservative (assumes FK relationships)\n * - 3: Moderate (recommended, safe middle ground)\n * - 10: SQLite's default (optimistic)\n */\n constructor(db: Database, defaultFanout = 3) {\n this.#db = db;\n this.#defaultFanout = defaultFanout;\n\n // Prepare SQL statements once for reuse across multiple getFanout() calls\n this.#stat4Stmt = this.#db.prepare(`\n SELECT neq, nlt, ndlt, sample\n FROM sqlite_stat4\n WHERE tbl = ? AND idx = ?\n ORDER BY nlt\n `);\n\n this.#stat1Stmt = this.#db.prepare(`\n SELECT stat\n FROM sqlite_stat1\n WHERE tbl = ? AND idx = ?\n `);\n\n this.#indexStmt = this.#db.prepare(`\n SELECT il.name as index_name, ii.seqno, ii.name as column_name\n FROM pragma_index_list(?) il\n JOIN pragma_index_info(il.name) ii\n ORDER BY il.seq, ii.seqno\n `);\n }\n\n /**\n * Gets the fanout factor for join column(s).\n *\n * Fanout = average number of child rows per distinct parent key value(s).\n *\n * ## Strategy\n *\n * 1. Try sqlite_stat4 (best): Histogram with separate NULL/non-NULL samples\n * 2. Fallback to sqlite_stat1: Average across all rows (includes NULLs)\n * 3. Fallback to default: When no statistics available\n *\n * ## Compound Indexes\n *\n * For multi-column joins, finds indexes where ALL columns appear as an\n * exact prefix. Uses the appropriate depth in stat1/stat4.\n *\n * Example:\n * - Columns: `['customerId', 'storeId']`\n * - Matches index: `(customerId, storeId, date)` at depth 2\n * - Uses stat1 parts[2] or stat4 neq[1] for accurate fanout\n *\n * ## Caching\n *\n * Results are cached per (table, columns) combination. Clear the cache if\n * you run ANALYZE to update statistics.\n *\n * @param tableName Table containing the join column(s)\n * @param columns Array of column names (one or more columns)\n * @returns Fanout result with value and source\n */\n getFanout(tableName: string, columns: string[]): FanoutResult {\n // Cache key uses sorted columns for consistency\n const cacheKey = `${tableName}:${[...columns].sort().join(',')}`;\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Strategy 1: Try stat4 first (most accurate)\n // NOTE: columns are NOT sorted - preserves Object.keys() order from constraint\n // Matching is order-independent (flexible), but we keep original order for consistency\n const stat4Result = this.#getFanoutFromStat4(tableName, columns);\n if (stat4Result) {\n this.#cache.set(cacheKey, stat4Result);\n return stat4Result;\n }\n\n // Strategy 2: Fallback to stat1 (includes NULLs)\n const stat1Result = this.#getFanoutFromStat1(tableName, columns);\n if (stat1Result) {\n this.#cache.set(cacheKey, stat1Result);\n return stat1Result;\n }\n\n // Strategy 3: Use default\n const defaultResult: FanoutResult = {\n fanout: this.#defaultFanout,\n confidence: 'none',\n source: 'default',\n };\n this.#cache.set(cacheKey, defaultResult);\n return defaultResult;\n }\n\n /**\n * Clears the fanout cache.\n * Call this after running ANALYZE to pick up updated statistics.\n */\n clearCache(): void {\n this.#cache.clear();\n }\n\n /**\n * Gets fanout from sqlite_stat4 histogram.\n *\n * Queries stat4 samples, decodes to identify NULLs, and returns\n * the median fanout of non-NULL samples.\n *\n * For compound indexes, uses the neq value at the appropriate depth.\n *\n * @param columns Array of column names to get fanout for\n * @returns Fanout result or undefined if stat4 unavailable\n */\n #getFanoutFromStat4(\n tableName: string,\n columns: string[],\n ): FanoutResult | undefined {\n try {\n // Find index containing the columns as a prefix\n const indexInfo = this.#findIndexForColumns(tableName, columns);\n if (!indexInfo) {\n return undefined;\n }\n\n // Query stat4 samples for this index (using prepared statement)\n const samples = this.#stat4Stmt.all(\n tableName,\n indexInfo.indexName,\n ) as Stat4Sample[];\n\n if (samples.length === 0) {\n return undefined;\n }\n\n // Decode samples and separate NULL from non-NULL\n // Use depth-1 for neq array index (depth is 1-based, array is 0-based)\n const neqIndex = indexInfo.depth - 1;\n const decodedSamples = samples.map(s => {\n const neqParts = s.neq.split(' ');\n return {\n fanout: parseInt(neqParts[neqIndex] ?? neqParts[0], 10),\n isNull: this.#decodeSampleIsNull(s.sample),\n };\n });\n\n const nonNullSamples = decodedSamples.filter(s => !s.isNull);\n\n if (nonNullSamples.length === 0) {\n // All samples are NULL - return fanout of 0 since NULLs don't match in joins\n return {\n fanout: 0,\n source: 'stat4',\n confidence: 'high',\n };\n }\n\n // Use median of non-NULL fanouts (more robust than average)\n const fanouts = nonNullSamples.map(s => s.fanout).sort((a, b) => a - b);\n const medianFanout =\n fanouts.length % 2 === 0\n ? Math.floor(\n (fanouts[fanouts.length / 2 - 1] + fanouts[fanouts.length / 2]) /\n 2,\n )\n : fanouts[Math.floor(fanouts.length / 2)];\n\n return {\n fanout: medianFanout,\n source: 'stat4',\n confidence: 'high',\n };\n } catch {\n // stat4 table may not exist or query may fail\n return undefined;\n }\n }\n\n /**\n * Gets fanout from sqlite_stat1 average.\n *\n * Note: This includes NULL rows in the calculation and may overestimate\n * fanout for sparse foreign keys.\n *\n * For compound indexes, uses the stat value at the appropriate depth.\n *\n * @param columns Array of column names to get fanout for\n * @returns Fanout result or undefined if stat1 unavailable\n */\n #getFanoutFromStat1(\n tableName: string,\n columns: string[],\n ): FanoutResult | undefined {\n try {\n // Find index containing the columns as a prefix\n const indexInfo = this.#findIndexForColumns(tableName, columns);\n if (!indexInfo) {\n return undefined;\n }\n\n // Query stat1 for this index (using prepared statement)\n const result = this.#stat1Stmt.get(tableName, indexInfo.indexName) as\n | {stat: string}\n | undefined;\n\n if (!result) {\n return undefined;\n }\n\n const parts = result.stat.split(' ');\n // Check if we have enough parts for the requested depth\n if (parts.length < indexInfo.depth + 1) {\n return undefined;\n }\n\n const fanout = parseInt(parts[indexInfo.depth], 10);\n if (isNaN(fanout)) {\n return undefined;\n }\n\n return {\n fanout,\n source: 'stat1',\n confidence: 'med',\n };\n } catch {\n return undefined;\n }\n }\n\n /**\n * Finds an index that can be used to get statistics for column(s).\n *\n * Uses pragma_index_list and pragma_index_info to reliably get index\n * column names, avoiding brittle SQL parsing. Includes all indices:\n * user-created (CREATE INDEX), PRIMARY KEY, and UNIQUE constraints.\n *\n * Uses flexible matching: Finds indexes where ALL columns appear in the\n * first N positions, regardless of order. This works because SQLite statistics\n * at depth N represent the fanout for the combination of the first N columns,\n * and combinations are order-independent.\n *\n * Example:\n * - columns: ['customerId', 'storeId']\n * - Matches: (customerId, storeId, date) at depth 2 ✅\n * - Matches: (storeId, customerId, date) at depth 2 ✅ (flexible order)\n * - Does NOT match: (date, customerId, storeId) ❌ (columns not in first 2 positions)\n * - Does NOT match: (customerId, date, storeId) ❌ (storeId not in first 2 positions)\n *\n * @param columns Array of column names (order-independent for matching)\n * @returns Index info with name and depth, or undefined if no match\n */\n #findIndexForColumns(\n tableName: string,\n columns: string[],\n ): {indexName: string; depth: number} | undefined {\n try {\n // Query returns all columns for all indexes (including PK/UNIQUE) in order\n const rows = this.#indexStmt.all(tableName) as {\n index_name: string;\n seqno: number;\n column_name: string;\n }[];\n\n // Group by index name\n const indexMap = new Map<string, string[]>();\n for (const row of rows) {\n const cols = indexMap.get(row.index_name) ?? [];\n cols.push(row.column_name);\n indexMap.set(row.index_name, cols);\n }\n\n // Check each index for prefix match\n for (const [indexName, indexColumns] of indexMap) {\n if (this.#isPrefixMatch(columns, indexColumns)) {\n return {\n indexName,\n depth: columns.length,\n };\n }\n }\n\n return undefined;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Checks if all queryColumns exist in the first N positions of indexColumns,\n * regardless of order.\n *\n * This allows flexible matching: constraint {a, b} matches both index (a, b, c)\n * and index (b, a, c) at depth 2, since both represent the fanout for the\n * combination of columns a and b.\n *\n * Gaps are NOT allowed: constraint {a, c} does NOT match index (a, b, c)\n * because no depth represents just (a, c) without b. Statistics are cumulative\n * from position 0.\n *\n * @param queryColumns Columns we're looking for (from constraint)\n * @param indexColumns Columns in the index (in order)\n * @returns true if all queryColumns exist in indexColumns[0...queryColumns.length-1]\n */\n #isPrefixMatch(queryColumns: string[], indexColumns: string[]): boolean {\n if (queryColumns.length > indexColumns.length) {\n return false;\n }\n\n // Get the prefix of the index that we're checking against\n const indexPrefix = indexColumns.slice(0, queryColumns.length);\n\n // Normalize to lowercase for case-insensitive comparison\n const indexPrefixLower = new Set(indexPrefix.map(col => col.toLowerCase()));\n const queryColumnsLower = queryColumns.map(col => col.toLowerCase());\n\n // Check if ALL query columns exist in the index prefix\n return queryColumnsLower.every(queryCol => indexPrefixLower.has(queryCol));\n }\n\n /**\n * Decodes a sqlite_stat4 sample value to check if it's NULL.\n *\n * SQLite record format (simplified):\n * - Varint: header size\n * - Serial types for each column (one byte each typically)\n * - Actual data\n *\n * Serial type 0 = NULL\n * Serial type 1 = 8-bit int\n * Serial type 2 = 16-bit int\n * Serial type 3 = 24-bit int\n * etc.\n *\n * We only need to check the first column's serial type.\n *\n * @param sample Binary-encoded sample from stat4\n * @returns true if the sample value is NULL\n */\n #decodeSampleIsNull(sample: Buffer): boolean {\n if (sample.length === 0) {\n return true;\n }\n\n // Read header size (varint - simplified: assume single byte)\n const headerSize = sample[0];\n\n if (headerSize === 0 || headerSize >= sample.length) {\n return true;\n }\n\n // Read first serial type (at position 1)\n const serialType = sample[1];\n\n // Serial type 0 = NULL\n return serialType === 0;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sqlite-stat-fanout.js","names":["#db","#defaultFanout","#cache","#stat4Stmt","#stat1Stmt","#indexStmt","#getFanoutFromStat4","#getFanoutFromStat1","#findIndexForColumns","#decodeSampleIsNull","#isPrefixMatch"],"sources":["../../../../zqlite/src/sqlite-stat-fanout.ts"],"sourcesContent":["import type {Database} from './db.ts';\n\n/**\n * Result of fanout calculation from SQLite statistics.\n */\nexport interface FanoutResult {\n /**\n * The fanout value (average rows per distinct value of the join column).\n * For non-NULL joins, this represents how many child rows exist per parent key.\n */\n fanout: number;\n confidence: 'high' | 'med' | 'none';\n\n /**\n * Source of the fanout calculation.\n * - 'stat4': From sqlite_stat4 histogram (most accurate, excludes NULLs)\n * - 'stat1': From sqlite_stat1 average (includes NULLs, may overestimate)\n * - 'default': Fallback constant when statistics unavailable\n */\n source: 'stat4' | 'stat1' | 'default';\n}\n\n/**\n * Sample from sqlite_stat4 histogram.\n */\ninterface Stat4Sample {\n /** \"N1 N2\" = rows equal to sample (N1=first col, N2=if composite) */\n neq: string;\n /** \"N1 N2\" = rows less than sample */\n nlt: string;\n /** \"N1 N2\" = distinct values less than sample */\n ndlt: string;\n /** The actual sample value (binary encoded) */\n sample: Buffer;\n}\n\n/**\n * Computes join fanout factors from SQLite statistics tables.\n *\n * Fanout is the average number of child rows per distinct parent key value,\n * used to estimate join cardinality in query planning.\n *\n * ## Problem\n *\n * sqlite_stat1 includes NULL rows in its calculation, which can significantly\n * overestimate fanout for sparse foreign keys:\n *\n * ```\n * Example: 100 tasks, 20 with project_id, 80 with NULL\n * - stat1 reports: \"100 17\" → fanout = 17 (WRONG - includes NULLs)\n * - stat4 shows: NULL samples with fanout=80, non-NULL samples with fanout=4\n * - True fanout: 4 (CORRECT)\n * ```\n *\n * ## Solution\n *\n * This class uses sqlite_stat4 histogram to separate NULL and non-NULL samples,\n * providing accurate fanout for non-NULL joins.\n *\n * ## Usage\n *\n * ```typescript\n * const calculator = new SQLiteStatFanout(db);\n *\n * // Get fanout for posts.userId → users.id join\n * const result = calculator.getFanout('posts', 'userId');\n *\n * if (result.source === 'stat4') {\n * // Accurate: excludes NULLs, samples actual distribution\n * console.log(`Fanout: ${result.fanout} (from stat4)`);\n * } else if (result.source === 'stat1') {\n * // Conservative: includes NULLs, may overestimate\n * console.log(`Fanout: ${result.fanout} (from stat1, includes NULLs)`);\n * } else {\n * // Fallback: no statistics available\n * console.log(`Fanout: ${result.fanout} (default estimate)`);\n * }\n * ```\n *\n * ## Requirements\n *\n * - SQLite compiled with ENABLE_STAT4 (most builds include this)\n * - `ANALYZE` command run on the database\n * - Index exists on the join column\n *\n * @see https://sqlite.org/fileformat2.html#stat4tab\n * @see packages/zql/src/planner/SELECTIVITY_PLAN.md\n */\nexport class SQLiteStatFanout {\n readonly #db: Database;\n readonly #defaultFanout: number;\n\n /**\n * Cache of fanout results by table and columns.\n * Key format: \"tableName:col1,col2,col3\" (sorted alphabetically)\n */\n readonly #cache = new Map<string, FanoutResult>();\n\n /**\n * Prepared statements for querying SQLite statistics tables.\n * Prepared once in constructor for performance.\n */\n readonly #stat4Stmt: ReturnType<Database['prepare']>;\n readonly #stat1Stmt: ReturnType<Database['prepare']>;\n readonly #indexStmt: ReturnType<Database['prepare']>;\n\n /**\n * Creates a new fanout calculator.\n *\n * @param db Database instance\n * @param defaultFanout Default fanout when statistics unavailable (default: 3)\n * - 1: Conservative (assumes FK relationships)\n * - 3: Moderate (recommended, safe middle ground)\n * - 10: SQLite's default (optimistic)\n */\n constructor(db: Database, defaultFanout = 3) {\n this.#db = db;\n this.#defaultFanout = defaultFanout;\n\n // Prepare SQL statements once for reuse across multiple getFanout() calls\n this.#stat4Stmt = this.#db.prepare(`\n SELECT neq, nlt, ndlt, sample\n FROM sqlite_stat4\n WHERE tbl = ? AND idx = ?\n ORDER BY nlt\n `);\n\n this.#stat1Stmt = this.#db.prepare(`\n SELECT stat\n FROM sqlite_stat1\n WHERE tbl = ? AND idx = ?\n `);\n\n this.#indexStmt = this.#db.prepare(`\n SELECT il.name as index_name, ii.seqno, ii.name as column_name\n FROM pragma_index_list(?) il\n JOIN pragma_index_info(il.name) ii\n ORDER BY il.seq, ii.seqno\n `);\n }\n\n /**\n * Gets the fanout factor for join column(s).\n *\n * Fanout = average number of child rows per distinct parent key value(s).\n *\n * ## Strategy\n *\n * 1. Try sqlite_stat4 (best): Histogram with separate NULL/non-NULL samples\n * 2. Fallback to sqlite_stat1: Average across all rows (includes NULLs)\n * 3. Fallback to default: When no statistics available\n *\n * ## Compound Indexes\n *\n * For multi-column joins, finds indexes where ALL columns appear as an\n * exact prefix. Uses the appropriate depth in stat1/stat4.\n *\n * Example:\n * - Columns: `['customerId', 'storeId']`\n * - Matches index: `(customerId, storeId, date)` at depth 2\n * - Uses stat1 parts[2] or stat4 neq[1] for accurate fanout\n *\n * ## Caching\n *\n * Results are cached per (table, columns) combination. Clear the cache if\n * you run ANALYZE to update statistics.\n *\n * @param tableName Table containing the join column(s)\n * @param columns Array of column names (one or more columns)\n * @returns Fanout result with value and source\n */\n getFanout(tableName: string, columns: string[]): FanoutResult {\n // Cache key uses sorted columns for consistency\n const cacheKey = `${tableName}:${columns.toSorted().join(',')}`;\n const cached = this.#cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Strategy 1: Try stat4 first (most accurate)\n // NOTE: columns are NOT sorted - preserves Object.keys() order from constraint\n // Matching is order-independent (flexible), but we keep original order for consistency\n const stat4Result = this.#getFanoutFromStat4(tableName, columns);\n if (stat4Result) {\n this.#cache.set(cacheKey, stat4Result);\n return stat4Result;\n }\n\n // Strategy 2: Fallback to stat1 (includes NULLs)\n const stat1Result = this.#getFanoutFromStat1(tableName, columns);\n if (stat1Result) {\n this.#cache.set(cacheKey, stat1Result);\n return stat1Result;\n }\n\n // Strategy 3: Use default\n const defaultResult: FanoutResult = {\n fanout: this.#defaultFanout,\n confidence: 'none',\n source: 'default',\n };\n this.#cache.set(cacheKey, defaultResult);\n return defaultResult;\n }\n\n /**\n * Clears the fanout cache.\n * Call this after running ANALYZE to pick up updated statistics.\n */\n clearCache(): void {\n this.#cache.clear();\n }\n\n /**\n * Gets fanout from sqlite_stat4 histogram.\n *\n * Queries stat4 samples, decodes to identify NULLs, and returns\n * the median fanout of non-NULL samples.\n *\n * For compound indexes, uses the neq value at the appropriate depth.\n *\n * @param columns Array of column names to get fanout for\n * @returns Fanout result or undefined if stat4 unavailable\n */\n #getFanoutFromStat4(\n tableName: string,\n columns: string[],\n ): FanoutResult | undefined {\n try {\n // Find index containing the columns as a prefix\n const indexInfo = this.#findIndexForColumns(tableName, columns);\n if (!indexInfo) {\n return undefined;\n }\n\n // Query stat4 samples for this index (using prepared statement)\n const samples = this.#stat4Stmt.all(\n tableName,\n indexInfo.indexName,\n ) as Stat4Sample[];\n\n if (samples.length === 0) {\n return undefined;\n }\n\n // Decode samples and separate NULL from non-NULL\n // Use depth-1 for neq array index (depth is 1-based, array is 0-based)\n const neqIndex = indexInfo.depth - 1;\n const decodedSamples = samples.map(s => {\n const neqParts = s.neq.split(' ');\n return {\n fanout: parseInt(neqParts[neqIndex] ?? neqParts[0], 10),\n isNull: this.#decodeSampleIsNull(s.sample),\n };\n });\n\n const nonNullSamples = decodedSamples.filter(s => !s.isNull);\n\n if (nonNullSamples.length === 0) {\n // All samples are NULL - return fanout of 0 since NULLs don't match in joins\n return {\n fanout: 0,\n source: 'stat4',\n confidence: 'high',\n };\n }\n\n // Use median of non-NULL fanouts (more robust than average)\n const fanouts = nonNullSamples.map(s => s.fanout).sort((a, b) => a - b);\n const medianFanout =\n fanouts.length % 2 === 0\n ? Math.floor(\n (fanouts[fanouts.length / 2 - 1] + fanouts[fanouts.length / 2]) /\n 2,\n )\n : fanouts[Math.floor(fanouts.length / 2)];\n\n return {\n fanout: medianFanout,\n source: 'stat4',\n confidence: 'high',\n };\n } catch {\n // stat4 table may not exist or query may fail\n return undefined;\n }\n }\n\n /**\n * Gets fanout from sqlite_stat1 average.\n *\n * Note: This includes NULL rows in the calculation and may overestimate\n * fanout for sparse foreign keys.\n *\n * For compound indexes, uses the stat value at the appropriate depth.\n *\n * @param columns Array of column names to get fanout for\n * @returns Fanout result or undefined if stat1 unavailable\n */\n #getFanoutFromStat1(\n tableName: string,\n columns: string[],\n ): FanoutResult | undefined {\n try {\n // Find index containing the columns as a prefix\n const indexInfo = this.#findIndexForColumns(tableName, columns);\n if (!indexInfo) {\n return undefined;\n }\n\n // Query stat1 for this index (using prepared statement)\n const result = this.#stat1Stmt.get(tableName, indexInfo.indexName) as\n | {stat: string}\n | undefined;\n\n if (!result) {\n return undefined;\n }\n\n const parts = result.stat.split(' ');\n // Check if we have enough parts for the requested depth\n if (parts.length < indexInfo.depth + 1) {\n return undefined;\n }\n\n const fanout = parseInt(parts[indexInfo.depth], 10);\n if (isNaN(fanout)) {\n return undefined;\n }\n\n return {\n fanout,\n source: 'stat1',\n confidence: 'med',\n };\n } catch {\n return undefined;\n }\n }\n\n /**\n * Finds an index that can be used to get statistics for column(s).\n *\n * Uses pragma_index_list and pragma_index_info to reliably get index\n * column names, avoiding brittle SQL parsing. Includes all indices:\n * user-created (CREATE INDEX), PRIMARY KEY, and UNIQUE constraints.\n *\n * Uses flexible matching: Finds indexes where ALL columns appear in the\n * first N positions, regardless of order. This works because SQLite statistics\n * at depth N represent the fanout for the combination of the first N columns,\n * and combinations are order-independent.\n *\n * Example:\n * - columns: ['customerId', 'storeId']\n * - Matches: (customerId, storeId, date) at depth 2 ✅\n * - Matches: (storeId, customerId, date) at depth 2 ✅ (flexible order)\n * - Does NOT match: (date, customerId, storeId) ❌ (columns not in first 2 positions)\n * - Does NOT match: (customerId, date, storeId) ❌ (storeId not in first 2 positions)\n *\n * @param columns Array of column names (order-independent for matching)\n * @returns Index info with name and depth, or undefined if no match\n */\n #findIndexForColumns(\n tableName: string,\n columns: string[],\n ): {indexName: string; depth: number} | undefined {\n try {\n // Query returns all columns for all indexes (including PK/UNIQUE) in order\n const rows = this.#indexStmt.all(tableName) as {\n index_name: string;\n seqno: number;\n column_name: string;\n }[];\n\n // Group by index name\n const indexMap = new Map<string, string[]>();\n for (const row of rows) {\n const cols = indexMap.get(row.index_name) ?? [];\n cols.push(row.column_name);\n indexMap.set(row.index_name, cols);\n }\n\n // Check each index for prefix match\n for (const [indexName, indexColumns] of indexMap) {\n if (this.#isPrefixMatch(columns, indexColumns)) {\n return {\n indexName,\n depth: columns.length,\n };\n }\n }\n\n return undefined;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Checks if all queryColumns exist in the first N positions of indexColumns,\n * regardless of order.\n *\n * This allows flexible matching: constraint {a, b} matches both index (a, b, c)\n * and index (b, a, c) at depth 2, since both represent the fanout for the\n * combination of columns a and b.\n *\n * Gaps are NOT allowed: constraint {a, c} does NOT match index (a, b, c)\n * because no depth represents just (a, c) without b. Statistics are cumulative\n * from position 0.\n *\n * @param queryColumns Columns we're looking for (from constraint)\n * @param indexColumns Columns in the index (in order)\n * @returns true if all queryColumns exist in indexColumns[0...queryColumns.length-1]\n */\n #isPrefixMatch(queryColumns: string[], indexColumns: string[]): boolean {\n if (queryColumns.length > indexColumns.length) {\n return false;\n }\n\n // Get the prefix of the index that we're checking against\n const indexPrefix = indexColumns.slice(0, queryColumns.length);\n\n // Normalize to lowercase for case-insensitive comparison\n const indexPrefixLower = new Set(indexPrefix.map(col => col.toLowerCase()));\n const queryColumnsLower = queryColumns.map(col => col.toLowerCase());\n\n // Check if ALL query columns exist in the index prefix\n return queryColumnsLower.every(queryCol => indexPrefixLower.has(queryCol));\n }\n\n /**\n * Decodes a sqlite_stat4 sample value to check if it's NULL.\n *\n * SQLite record format (simplified):\n * - Varint: header size\n * - Serial types for each column (one byte each typically)\n * - Actual data\n *\n * Serial type 0 = NULL\n * Serial type 1 = 8-bit int\n * Serial type 2 = 16-bit int\n * Serial type 3 = 24-bit int\n * etc.\n *\n * We only need to check the first column's serial type.\n *\n * @param sample Binary-encoded sample from stat4\n * @returns true if the sample value is NULL\n */\n #decodeSampleIsNull(sample: Buffer): boolean {\n if (sample.length === 0) {\n return true;\n }\n\n // Read header size (varint - simplified: assume single byte)\n const headerSize = sample[0];\n\n if (headerSize === 0 || headerSize >= sample.length) {\n return true;\n }\n\n // Read first serial type (at position 1)\n const serialType = sample[1];\n\n // Serial type 0 = NULL\n return serialType === 0;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-source.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/table-source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,+BAA+B,CAAC;AAK7D,OAAO,KAAK,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAC,GAAG,EAAQ,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACV,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,yCAAyC,CAAC;AAgB3E,OAAO,EACL,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"table-source.d.ts","sourceRoot":"","sources":["../../../../zqlite/src/table-source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,+BAA+B,CAAC;AAK7D,OAAO,KAAK,EAAC,SAAS,EAAE,QAAQ,EAAC,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAC,GAAG,EAAQ,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACV,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,yCAAyC,CAAC;AAgB3E,OAAO,EACL,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,6BAA6B,CAAC;AAExD,OAAO,KAAK,EAAC,QAAQ,EAAY,MAAM,SAAS,CAAC;AAoBjD;;;;;;;;;;;;;GAaG;AACH,qBAAa,WAAY,YAAW,MAAM;;IAexC;;;;;OAKG;gBAED,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,SAAS,EACpB,EAAE,EAAE,QAAQ,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACpC,UAAU,EAAE,UAAU,EACtB,WAAW,gBAAc;IAiB3B,IAAI,WAAW;;;;MAMd;IAED;;;OAGG;IACH,KAAK,CAAC,EAAE,EAAE,QAAQ;IA4FlB,OAAO,CACL,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,OAAO,CAAC,EAAE,SAAS,EACnB,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,EAC3B,KAAK,CAAC,EAAE,aAAa;IA8CvB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG;IAoHzB,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC;IAQ3C,OAAO,CAAC,MAAM,EAAE,YAAY;IA4F7B;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS;CA8BrC;AA6BD,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,GAAG,EAAE,GAAG,EACR,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GACvC,SAAS,OAAO,EAAE,CAEpB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,wCAa/C;AAED,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACvC,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,MAAM,GAChB,GAAG,CAaL;AAwCD,qBAAa,qBAAsB,SAAQ,KAAK;CAAG"}
|
|
@@ -53,7 +53,7 @@ var TableSource = class {
|
|
|
53
53
|
this.#primaryKey = primaryKey;
|
|
54
54
|
this.#stmts = this.#getStatementsFor(db);
|
|
55
55
|
this.#shouldYield = shouldYield;
|
|
56
|
-
assert(this.#uniqueIndexes.has(JSON.stringify(
|
|
56
|
+
assert(this.#uniqueIndexes.has(JSON.stringify(primaryKey.toSorted())), `primary key ${primaryKey} does not have a UNIQUE index`);
|
|
57
57
|
}
|
|
58
58
|
get tableSchema() {
|
|
59
59
|
return {
|
|
@@ -166,17 +166,13 @@ var TableSource = class {
|
|
|
166
166
|
}
|
|
167
167
|
*#mapFromSQLiteTypes(valueTypes, rowIterator, query, debug) {
|
|
168
168
|
let result;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} while (!result.done);
|
|
177
|
-
} finally {
|
|
178
|
-
rowIterator.return?.();
|
|
179
|
-
}
|
|
169
|
+
do {
|
|
170
|
+
result = timeSampled(this.#lc, ++eventCount, this.#logConfig.ivmSampling, () => rowIterator.next(), this.#logConfig.slowRowThreshold, () => `table-source.next took too long for ${query}. Are you missing an index?`);
|
|
171
|
+
if (result.done) break;
|
|
172
|
+
const row = fromSQLiteTypes(valueTypes, result.value, this.#table);
|
|
173
|
+
debug?.rowVended(this.#table, query, row);
|
|
174
|
+
yield row;
|
|
175
|
+
} while (!result.done);
|
|
180
176
|
}
|
|
181
177
|
*push(change) {
|
|
182
178
|
for (const result of this.genPush(change)) if (result === "yield") yield result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-source.js","names":["#dbCache","#connections","#table","#columns","#uniqueIndexes","#primaryKey","#logConfig","#lc","#shouldYield","#stmts","#getStatementsFor","#allColumns","#fetch","#getSchema","#requestToSQL","#mapFromSQLiteTypes","#overlay","#writeChange","#pushEpoch","#getRowStmtCache","#getRowStmt"],"sources":["../../../../zqlite/src/table-source.ts"],"sourcesContent":["import type {SQLQuery} from '@databases/sql';\nimport type {LogContext} from '@rocicorp/logger';\nimport SQLite3Database from '@rocicorp/zero-sqlite3';\nimport type {LogConfig} from '../../otel/src/log-options.ts';\nimport {timeSampled} from '../../otel/src/maybe-time.ts';\nimport {assert, unreachable} from '../../shared/src/asserts.ts';\nimport {must} from '../../shared/src/must.ts';\nimport type {Writable} from '../../shared/src/writable.ts';\nimport type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {Row, Value} from '../../zero-protocol/src/data.ts';\nimport type {PrimaryKey} from '../../zero-protocol/src/primary-key.ts';\nimport type {\n SchemaValue,\n ValueType,\n} from '../../zero-schema/src/table-schema.ts';\nimport type {DebugDelegate} from '../../zql/src/builder/debug-delegate.ts';\nimport {\n createPredicate,\n transformFilters,\n} from '../../zql/src/builder/filter.ts';\nimport {makeComparator, type Node} from '../../zql/src/ivm/data.ts';\nimport {\n generateWithOverlay,\n generateWithOverlayUnordered,\n generateWithStart,\n genPushAndWriteWithSplitEdit,\n type Connection,\n type Overlay,\n} from '../../zql/src/ivm/memory-source.ts';\nimport {type FetchRequest} from '../../zql/src/ivm/operator.ts';\nimport type {SourceSchema} from '../../zql/src/ivm/schema.ts';\nimport {\n type Source,\n type SourceChange,\n type SourceInput,\n} from '../../zql/src/ivm/source.ts';\nimport type {Stream} from '../../zql/src/ivm/stream.ts';\nimport type {Database, Statement} from './db.ts';\nimport {compile, format, sql} from './internal/sql.ts';\nimport {StatementCache} from './internal/statement-cache.ts';\nimport {\n buildSelectQuery,\n toSQLiteType,\n type NoSubqueryCondition,\n} from './query-builder.ts';\nimport {assertOrderingIncludesPK} from '../../zql/src/query/complete-ordering.ts';\n\ntype Statements = {\n readonly cache: StatementCache;\n readonly insert: Statement;\n readonly delete: Statement;\n readonly update: Statement | undefined;\n readonly checkExists: Statement;\n readonly getExisting: Statement;\n};\n\nlet eventCount = 0;\n\n/**\n * A source that is backed by a SQLite table.\n *\n * Values are written to the backing table _after_ being vended by the source.\n *\n * This ordering of events is to ensure self joins function properly. That is,\n * we can't reveal a value to an output before it has been pushed to that output.\n *\n * The code is fairly straightforward except for:\n * 1. Dealing with a `fetch` that has a basis of `before`.\n * 2. Dealing with compound orders that have differing directions (a ASC, b DESC, c ASC)\n *\n * See comments in relevant functions for more details.\n */\nexport class TableSource implements Source {\n readonly #dbCache = new WeakMap<Database, Statements>();\n readonly #connections: Connection[] = [];\n readonly #table: string;\n readonly #columns: Record<string, SchemaValue>;\n // Maps sorted columns JSON string (e.g. '[\"a\",\"b\"]) to Set of columns.\n readonly #uniqueIndexes: Map<string, Set<string>>;\n readonly #primaryKey: PrimaryKey;\n readonly #logConfig: LogConfig;\n readonly #lc: LogContext;\n readonly #shouldYield: () => boolean;\n #stmts: Statements;\n #overlay?: Overlay | undefined;\n #pushEpoch = 0;\n\n /**\n * @param shouldYield a function called after each row is read from the database,\n * which should return true if the source should yield the special 'yield' value\n * to yield control back to the caller at the end of the pipeline. Can\n * also throw an error to abort the pipeline processing.\n */\n constructor(\n logContext: LogContext,\n logConfig: LogConfig,\n db: Database,\n tableName: string,\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n shouldYield = () => false,\n ) {\n this.#lc = logContext;\n this.#logConfig = logConfig;\n this.#table = tableName;\n this.#columns = columns;\n this.#uniqueIndexes = getUniqueIndexes(db, tableName);\n this.#primaryKey = primaryKey;\n this.#stmts = this.#getStatementsFor(db);\n this.#shouldYield = shouldYield;\n\n assert(\n this.#uniqueIndexes.has(JSON.stringify([...primaryKey].sort())),\n `primary key ${primaryKey} does not have a UNIQUE index`,\n );\n }\n\n get tableSchema() {\n return {\n name: this.#table,\n columns: this.#columns,\n primaryKey: this.#primaryKey,\n };\n }\n\n /**\n * Sets the db (snapshot) to use, to facilitate the Snapshotter leapfrog\n * algorithm for concurrent traversal of historic timelines.\n */\n setDB(db: Database) {\n this.#stmts = this.#getStatementsFor(db);\n }\n\n #getStatementsFor(db: Database) {\n const cached = this.#dbCache.get(db);\n if (cached) {\n return cached;\n }\n\n const stmts = {\n cache: new StatementCache(db),\n insert: db.prepare(\n compile(\n sql`INSERT INTO ${sql.ident(this.#table)} (${sql.join(\n Object.keys(this.#columns).map(c => sql.ident(c)),\n ', ',\n )}) VALUES (${sql.__dangerous__rawValue(\n Array.from({length: Object.keys(this.#columns).length})\n .fill('?')\n .join(','),\n )})`,\n ),\n ),\n delete: db.prepare(\n compile(\n sql`DELETE FROM ${sql.ident(this.#table)} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )}`,\n ),\n ),\n // If all the columns are part of the primary key, we cannot use UPDATE.\n update:\n Object.keys(this.#columns).length > this.#primaryKey.length\n ? db.prepare(\n compile(\n sql`UPDATE ${sql.ident(this.#table)} SET ${sql.join(\n nonPrimaryKeys(this.#columns, this.#primaryKey).map(\n c => sql`${sql.ident(c)}=?`,\n ),\n ',',\n )} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )}`,\n ),\n )\n : undefined,\n checkExists: db.prepare(\n compile(\n sql`SELECT 1 AS \"exists\" FROM ${sql.ident(\n this.#table,\n )} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )} LIMIT 1`,\n ),\n ),\n getExisting: db.prepare(\n compile(\n sql`SELECT * FROM ${sql.ident(this.#table)} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )}`,\n ),\n ),\n };\n this.#dbCache.set(db, stmts);\n return stmts;\n }\n\n get #allColumns() {\n return sql.join(\n Object.keys(this.#columns).map(c => sql.ident(c)),\n sql`,`,\n );\n }\n\n #getSchema(connection: Connection, unordered: boolean): SourceSchema {\n return {\n tableName: this.#table,\n columns: this.#columns,\n primaryKey: this.#primaryKey,\n sort: unordered ? undefined : connection.sort,\n relationships: {},\n isHidden: false,\n system: 'client',\n compareRows: connection.compareRows,\n };\n }\n\n connect(\n sort: Ordering | undefined,\n filters?: Condition,\n splitEditKeys?: Set<string>,\n debug?: DebugDelegate,\n ) {\n const transformedFilters = transformFilters(filters);\n const unordered = sort === undefined;\n // PK comparator is used for source-level overlay matching (remove by PK\n // equality) even when no ordering is requested.\n const primaryKeySort: Ordering = this.#primaryKey.map(k => [k, 'asc']);\n\n const input: SourceInput = {\n getSchema: () => schema,\n fetch: req => this.#fetch(req, connection),\n setOutput: output => {\n connection.output = output;\n },\n destroy: () => {\n const idx = this.#connections.indexOf(connection);\n assert(idx !== -1, 'Connection not found');\n this.#connections.splice(idx, 1);\n },\n fullyAppliedFilters: !transformedFilters.conditionsRemoved,\n };\n\n const connection: Connection = {\n input,\n debug,\n output: undefined,\n sort,\n splitEditKeys,\n filters: transformedFilters.filters\n ? {\n condition: transformedFilters.filters,\n predicate: createPredicate(transformedFilters.filters),\n }\n : undefined,\n compareRows: sort ? makeComparator(sort) : makeComparator(primaryKeySort),\n lastPushedEpoch: 0,\n };\n const schema = this.#getSchema(connection, unordered);\n if (!unordered) {\n assertOrderingIncludesPK(sort, this.#primaryKey);\n }\n\n this.#connections.push(connection);\n return input;\n }\n\n toSQLiteRow(row: Row): Row {\n return Object.fromEntries(\n Object.entries(row).map(([key, value]) => [\n key,\n toSQLiteType(value, this.#columns[key].type),\n ]),\n ) as Row;\n }\n\n *#fetch(req: FetchRequest, connection: Connection): Stream<Node | 'yield'> {\n const {sort, debug} = connection;\n\n const query = this.#requestToSQL(req, connection.filters?.condition, sort);\n const sqlAndBindings = format(query);\n\n const cachedStatement = this.#stmts.cache.get(sqlAndBindings.text);\n cachedStatement.statement.safeIntegers(true);\n const rowIterator = cachedStatement.statement.iterate<Row>(\n ...sqlAndBindings.values,\n );\n try {\n debug?.initQuery(this.#table, sqlAndBindings.text);\n\n if (sort) {\n const comparator = makeComparator(sort, req.reverse);\n yield* generateWithStart(\n generateWithYields(\n generateWithOverlay(\n req.start?.row,\n this.#mapFromSQLiteTypes(\n this.#columns,\n rowIterator,\n sqlAndBindings.text,\n debug,\n ),\n req.constraint,\n this.#overlay,\n connection.lastPushedEpoch,\n comparator,\n connection.filters?.predicate,\n ),\n this.#shouldYield,\n ),\n req.start,\n comparator,\n );\n } else {\n yield* generateWithYields(\n generateWithOverlayUnordered(\n this.#mapFromSQLiteTypes(\n this.#columns,\n rowIterator,\n sqlAndBindings.text,\n debug,\n ),\n req.constraint,\n this.#overlay,\n connection.lastPushedEpoch,\n this.#primaryKey,\n connection.filters?.predicate,\n ),\n this.#shouldYield,\n );\n }\n } finally {\n // Ensure the SQLite iterate() is closed. Normally #mapFromSQLiteTypes\n // closes it via its own finally block, but if the generator chain is\n // returned before #mapFromSQLiteTypes was ever started (e.g., the\n // unordered overlay yielded an add row before iterating the source),\n // the SQLite iterator would remain active and lock the connection.\n // Calling return() on an already-closed iterator is a safe no-op.\n rowIterator.return?.();\n if (debug) {\n let totalNvisit = 0;\n let i = 0;\n while (true) {\n const nvisit = cachedStatement.statement.scanStatus(\n i++,\n SQLite3Database.SQLITE_SCANSTAT_NVISIT,\n 1,\n );\n if (nvisit === undefined) {\n break;\n }\n totalNvisit += Number(nvisit);\n }\n if (totalNvisit !== 0) {\n debug.recordNVisit(this.#table, sqlAndBindings.text, totalNvisit);\n }\n cachedStatement.statement.scanStatusReset();\n }\n this.#stmts.cache.return(cachedStatement);\n }\n }\n\n *#mapFromSQLiteTypes(\n valueTypes: Record<string, SchemaValue>,\n rowIterator: IterableIterator<Row>,\n query: string,\n debug: DebugDelegate | undefined,\n ): IterableIterator<Row> {\n let result;\n try {\n do {\n result = timeSampled(\n this.#lc,\n ++eventCount,\n this.#logConfig.ivmSampling,\n () => rowIterator.next(),\n this.#logConfig.slowRowThreshold,\n () =>\n `table-source.next took too long for ${query}. Are you missing an index?`,\n );\n if (result.done) {\n break;\n }\n const row = fromSQLiteTypes(valueTypes, result.value, this.#table);\n debug?.rowVended(this.#table, query, row);\n yield row;\n } while (!result.done);\n } finally {\n rowIterator.return?.();\n }\n }\n\n *push(change: SourceChange): Stream<'yield'> {\n for (const result of this.genPush(change)) {\n if (result === 'yield') {\n yield result;\n }\n }\n }\n\n *genPush(change: SourceChange) {\n const exists = (row: Row) =>\n this.#stmts.checkExists.get<{exists: number} | undefined>(\n ...toSQLiteTypes(this.#primaryKey, row, this.#columns),\n )?.exists === 1;\n const setOverlay = (o: Overlay | undefined) => (this.#overlay = o);\n const writeChange = (c: SourceChange) => this.#writeChange(c);\n\n yield* genPushAndWriteWithSplitEdit(\n this.#connections,\n change,\n exists,\n setOverlay,\n writeChange,\n () => ++this.#pushEpoch,\n );\n }\n\n #writeChange(change: SourceChange) {\n switch (change.type) {\n case 'add':\n this.#stmts.insert.run(\n ...toSQLiteTypes(\n Object.keys(this.#columns),\n change.row,\n this.#columns,\n ),\n );\n break;\n case 'remove':\n this.#stmts.delete.run(\n ...toSQLiteTypes(this.#primaryKey, change.row, this.#columns),\n );\n break;\n case 'edit': {\n // If the PK is the same, use UPDATE.\n if (\n canUseUpdate(\n change.oldRow,\n change.row,\n this.#columns,\n this.#primaryKey,\n )\n ) {\n const mergedRow = {\n ...change.oldRow,\n ...change.row,\n };\n const params = [\n ...nonPrimaryValues(this.#columns, this.#primaryKey, mergedRow),\n ...toSQLiteTypes(this.#primaryKey, mergedRow, this.#columns),\n ];\n must(this.#stmts.update).run(params);\n } else {\n this.#stmts.delete.run(\n ...toSQLiteTypes(this.#primaryKey, change.oldRow, this.#columns),\n );\n this.#stmts.insert.run(\n ...toSQLiteTypes(\n Object.keys(this.#columns),\n change.row,\n this.#columns,\n ),\n );\n }\n\n break;\n }\n default:\n unreachable(change);\n }\n }\n\n #getRowStmtCache = new Map<string, string>();\n\n #getRowStmt(keyCols: string[]): string {\n const keyString = JSON.stringify(keyCols);\n let stmt = this.#getRowStmtCache.get(keyString);\n if (!stmt) {\n stmt = compile(\n sql`SELECT ${this.#allColumns} FROM ${sql.ident(\n this.#table,\n )} WHERE ${sql.join(\n keyCols.map(k => sql`${sql.ident(k)}=?`),\n sql` AND`,\n )}`,\n );\n this.#getRowStmtCache.set(keyString, stmt);\n }\n return stmt;\n }\n\n /**\n * Retrieves a row from the backing DB by a unique key, or `undefined` if such a\n * row does not exist. This is not used in the IVM pipeline but is useful\n * for retrieving data that is consistent with the state (and type\n * semantics) of the pipeline. Note that this key may not necessarily correspond\n * to the `primaryKey` with which this TableSource.\n */\n getRow(rowKey: Row): Row | undefined {\n const keyCols = Object.keys(rowKey);\n\n const stmt = this.#getRowStmt(keyCols);\n const row = this.#stmts.cache.use(stmt, cached =>\n cached.statement\n .safeIntegers(true)\n .get<Row>(...toSQLiteTypes(keyCols, rowKey, this.#columns)),\n );\n if (row) {\n return fromSQLiteTypes(this.#columns, row, this.#table);\n }\n return row;\n }\n\n #requestToSQL(\n request: FetchRequest,\n filters: NoSubqueryCondition | undefined,\n order: Ordering | undefined,\n ): SQLQuery {\n return buildSelectQuery(\n this.#table,\n this.#columns,\n request.constraint,\n filters,\n order,\n request.reverse,\n request.start,\n );\n }\n}\n\nfunction getUniqueIndexes(\n db: Database,\n tableName: string,\n): Map<string, Set<string>> {\n const sqlAndBindings = format(\n sql`\n SELECT idx.name, json_group_array(col.name) as columnsJSON\n FROM sqlite_master as idx\n JOIN pragma_index_list(idx.tbl_name) AS info ON info.name = idx.name\n JOIN pragma_index_info(idx.name) as col\n WHERE idx.tbl_name = ${tableName} AND\n idx.type = 'index' AND \n info.\"unique\" != 0\n GROUP BY idx.name\n ORDER BY idx.name`,\n );\n const stmt = db.prepare(sqlAndBindings.text);\n const indexes = stmt.all<{columnsJSON: string}>(...sqlAndBindings.values);\n return new Map(\n indexes.map(({columnsJSON}) => {\n const columns = JSON.parse(columnsJSON);\n const set = new Set<string>(columns);\n return [JSON.stringify(columns.sort()), set];\n }),\n );\n}\n\nexport function toSQLiteTypes(\n columns: readonly string[],\n row: Row,\n columnTypes: Record<string, SchemaValue>,\n): readonly unknown[] {\n return columns.map(col => toSQLiteType(row[col], columnTypes[col].type));\n}\n\nexport function toSQLiteTypeName(type: ValueType) {\n switch (type) {\n case 'boolean':\n return 'INTEGER';\n case 'number':\n return 'REAL';\n case 'string':\n return 'TEXT';\n case 'null':\n return 'NULL';\n case 'json':\n return 'TEXT';\n }\n}\n\nexport function fromSQLiteTypes(\n valueTypes: Record<string, SchemaValue>,\n row: Row,\n tableName: string,\n): Row {\n const newRow: Writable<Row> = {};\n for (const key of Object.keys(row)) {\n const valueType = valueTypes[key];\n if (valueType === undefined) {\n const columnList = Object.keys(valueTypes).sort().join(', ');\n throw new Error(\n `Invalid column \"${key}\" for table \"${tableName}\". Synced columns include ${columnList}`,\n );\n }\n newRow[key] = fromSQLiteType(valueType.type, row[key], key, tableName);\n }\n return newRow;\n}\n\nfunction fromSQLiteType(\n valueType: ValueType,\n v: Value,\n column: string,\n tableName: string,\n): Value {\n if (v === null) {\n return null;\n }\n switch (valueType) {\n case 'boolean':\n return !!v;\n case 'number':\n case 'string':\n case 'null':\n if (typeof v === 'bigint') {\n if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {\n throw new UnsupportedValueError(\n `value ${v} (in ${tableName}.${column}) is outside of supported bounds`,\n );\n }\n return Number(v);\n }\n return v;\n case 'json':\n try {\n return JSON.parse(v as string);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n throw new UnsupportedValueError(\n `Failed to parse JSON for ${tableName}.${column}: ${errorMessage}`,\n {cause: error},\n );\n }\n }\n}\n\nexport class UnsupportedValueError extends Error {}\n\nfunction canUseUpdate(\n oldRow: Row,\n row: Row,\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n): boolean {\n for (const pk of primaryKey) {\n if (oldRow[pk] !== row[pk]) {\n return false;\n }\n }\n return Object.keys(columns).length > primaryKey.length;\n}\n\nfunction nonPrimaryValues(\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n row: Row,\n): Iterable<unknown> {\n return nonPrimaryKeys(columns, primaryKey).map(c =>\n toSQLiteType(row[c], columns[c].type),\n );\n}\n\nfunction nonPrimaryKeys(\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n) {\n return Object.keys(columns).filter(c => !primaryKey.includes(c));\n}\n\nfunction* generateWithYields(stream: Stream<Node>, shouldYield: () => boolean) {\n for (const n of stream) {\n if (shouldYield()) {\n yield 'yield';\n }\n yield n;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"table-source.js","names":["#dbCache","#connections","#table","#columns","#uniqueIndexes","#primaryKey","#logConfig","#lc","#shouldYield","#stmts","#getStatementsFor","#allColumns","#fetch","#getSchema","#requestToSQL","#mapFromSQLiteTypes","#overlay","#writeChange","#pushEpoch","#getRowStmtCache","#getRowStmt"],"sources":["../../../../zqlite/src/table-source.ts"],"sourcesContent":["import type {SQLQuery} from '@databases/sql';\nimport type {LogContext} from '@rocicorp/logger';\nimport SQLite3Database from '@rocicorp/zero-sqlite3';\nimport type {LogConfig} from '../../otel/src/log-options.ts';\nimport {timeSampled} from '../../otel/src/maybe-time.ts';\nimport {assert, unreachable} from '../../shared/src/asserts.ts';\nimport {must} from '../../shared/src/must.ts';\nimport type {Writable} from '../../shared/src/writable.ts';\nimport type {Condition, Ordering} from '../../zero-protocol/src/ast.ts';\nimport type {Row, Value} from '../../zero-protocol/src/data.ts';\nimport type {PrimaryKey} from '../../zero-protocol/src/primary-key.ts';\nimport type {\n SchemaValue,\n ValueType,\n} from '../../zero-schema/src/table-schema.ts';\nimport type {DebugDelegate} from '../../zql/src/builder/debug-delegate.ts';\nimport {\n createPredicate,\n transformFilters,\n} from '../../zql/src/builder/filter.ts';\nimport {makeComparator, type Node} from '../../zql/src/ivm/data.ts';\nimport {\n generateWithOverlay,\n generateWithOverlayUnordered,\n generateWithStart,\n genPushAndWriteWithSplitEdit,\n type Connection,\n type Overlay,\n} from '../../zql/src/ivm/memory-source.ts';\nimport {type FetchRequest} from '../../zql/src/ivm/operator.ts';\nimport type {SourceSchema} from '../../zql/src/ivm/schema.ts';\nimport {\n type Source,\n type SourceChange,\n type SourceInput,\n} from '../../zql/src/ivm/source.ts';\nimport type {Stream} from '../../zql/src/ivm/stream.ts';\nimport {assertOrderingIncludesPK} from '../../zql/src/query/complete-ordering.ts';\nimport type {Database, Statement} from './db.ts';\nimport {compile, format, sql} from './internal/sql.ts';\nimport {StatementCache} from './internal/statement-cache.ts';\nimport {\n buildSelectQuery,\n toSQLiteType,\n type NoSubqueryCondition,\n} from './query-builder.ts';\n\ntype Statements = {\n readonly cache: StatementCache;\n readonly insert: Statement;\n readonly delete: Statement;\n readonly update: Statement | undefined;\n readonly checkExists: Statement;\n readonly getExisting: Statement;\n};\n\nlet eventCount = 0;\n\n/**\n * A source that is backed by a SQLite table.\n *\n * Values are written to the backing table _after_ being vended by the source.\n *\n * This ordering of events is to ensure self joins function properly. That is,\n * we can't reveal a value to an output before it has been pushed to that output.\n *\n * The code is fairly straightforward except for:\n * 1. Dealing with a `fetch` that has a basis of `before`.\n * 2. Dealing with compound orders that have differing directions (a ASC, b DESC, c ASC)\n *\n * See comments in relevant functions for more details.\n */\nexport class TableSource implements Source {\n readonly #dbCache = new WeakMap<Database, Statements>();\n readonly #connections: Connection[] = [];\n readonly #table: string;\n readonly #columns: Record<string, SchemaValue>;\n // Maps sorted columns JSON string (e.g. '[\"a\",\"b\"]) to Set of columns.\n readonly #uniqueIndexes: Map<string, Set<string>>;\n readonly #primaryKey: PrimaryKey;\n readonly #logConfig: LogConfig;\n readonly #lc: LogContext;\n readonly #shouldYield: () => boolean;\n #stmts: Statements;\n #overlay?: Overlay | undefined;\n #pushEpoch = 0;\n\n /**\n * @param shouldYield a function called after each row is read from the database,\n * which should return true if the source should yield the special 'yield' value\n * to yield control back to the caller at the end of the pipeline. Can\n * also throw an error to abort the pipeline processing.\n */\n constructor(\n logContext: LogContext,\n logConfig: LogConfig,\n db: Database,\n tableName: string,\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n shouldYield = () => false,\n ) {\n this.#lc = logContext;\n this.#logConfig = logConfig;\n this.#table = tableName;\n this.#columns = columns;\n this.#uniqueIndexes = getUniqueIndexes(db, tableName);\n this.#primaryKey = primaryKey;\n this.#stmts = this.#getStatementsFor(db);\n this.#shouldYield = shouldYield;\n\n assert(\n this.#uniqueIndexes.has(JSON.stringify(primaryKey.toSorted())),\n `primary key ${primaryKey} does not have a UNIQUE index`,\n );\n }\n\n get tableSchema() {\n return {\n name: this.#table,\n columns: this.#columns,\n primaryKey: this.#primaryKey,\n };\n }\n\n /**\n * Sets the db (snapshot) to use, to facilitate the Snapshotter leapfrog\n * algorithm for concurrent traversal of historic timelines.\n */\n setDB(db: Database) {\n this.#stmts = this.#getStatementsFor(db);\n }\n\n #getStatementsFor(db: Database) {\n const cached = this.#dbCache.get(db);\n if (cached) {\n return cached;\n }\n\n const stmts = {\n cache: new StatementCache(db),\n insert: db.prepare(\n compile(\n sql`INSERT INTO ${sql.ident(this.#table)} (${sql.join(\n Object.keys(this.#columns).map(c => sql.ident(c)),\n ', ',\n )}) VALUES (${sql.__dangerous__rawValue(\n Array.from({length: Object.keys(this.#columns).length})\n .fill('?')\n .join(','),\n )})`,\n ),\n ),\n delete: db.prepare(\n compile(\n sql`DELETE FROM ${sql.ident(this.#table)} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )}`,\n ),\n ),\n // If all the columns are part of the primary key, we cannot use UPDATE.\n update:\n Object.keys(this.#columns).length > this.#primaryKey.length\n ? db.prepare(\n compile(\n sql`UPDATE ${sql.ident(this.#table)} SET ${sql.join(\n nonPrimaryKeys(this.#columns, this.#primaryKey).map(\n c => sql`${sql.ident(c)}=?`,\n ),\n ',',\n )} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )}`,\n ),\n )\n : undefined,\n checkExists: db.prepare(\n compile(\n sql`SELECT 1 AS \"exists\" FROM ${sql.ident(\n this.#table,\n )} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )} LIMIT 1`,\n ),\n ),\n getExisting: db.prepare(\n compile(\n sql`SELECT * FROM ${sql.ident(this.#table)} WHERE ${sql.join(\n this.#primaryKey.map(k => sql`${sql.ident(k)}=?`),\n ' AND ',\n )}`,\n ),\n ),\n };\n this.#dbCache.set(db, stmts);\n return stmts;\n }\n\n get #allColumns() {\n return sql.join(\n Object.keys(this.#columns).map(c => sql.ident(c)),\n sql`,`,\n );\n }\n\n #getSchema(connection: Connection, unordered: boolean): SourceSchema {\n return {\n tableName: this.#table,\n columns: this.#columns,\n primaryKey: this.#primaryKey,\n sort: unordered ? undefined : connection.sort,\n relationships: {},\n isHidden: false,\n system: 'client',\n compareRows: connection.compareRows,\n };\n }\n\n connect(\n sort: Ordering | undefined,\n filters?: Condition,\n splitEditKeys?: Set<string>,\n debug?: DebugDelegate,\n ) {\n const transformedFilters = transformFilters(filters);\n const unordered = sort === undefined;\n // PK comparator is used for source-level overlay matching (remove by PK\n // equality) even when no ordering is requested.\n const primaryKeySort: Ordering = this.#primaryKey.map(k => [k, 'asc']);\n\n const input: SourceInput = {\n getSchema: () => schema,\n fetch: req => this.#fetch(req, connection),\n setOutput: output => {\n connection.output = output;\n },\n destroy: () => {\n const idx = this.#connections.indexOf(connection);\n assert(idx !== -1, 'Connection not found');\n this.#connections.splice(idx, 1);\n },\n fullyAppliedFilters: !transformedFilters.conditionsRemoved,\n };\n\n const connection: Connection = {\n input,\n debug,\n output: undefined,\n sort,\n splitEditKeys,\n filters: transformedFilters.filters\n ? {\n condition: transformedFilters.filters,\n predicate: createPredicate(transformedFilters.filters),\n }\n : undefined,\n compareRows: sort ? makeComparator(sort) : makeComparator(primaryKeySort),\n lastPushedEpoch: 0,\n };\n const schema = this.#getSchema(connection, unordered);\n if (!unordered) {\n assertOrderingIncludesPK(sort, this.#primaryKey);\n }\n\n this.#connections.push(connection);\n return input;\n }\n\n toSQLiteRow(row: Row): Row {\n return Object.fromEntries(\n Object.entries(row).map(([key, value]) => [\n key,\n toSQLiteType(value, this.#columns[key].type),\n ]),\n ) as Row;\n }\n\n *#fetch(req: FetchRequest, connection: Connection): Stream<Node | 'yield'> {\n const {sort, debug} = connection;\n\n const query = this.#requestToSQL(req, connection.filters?.condition, sort);\n const sqlAndBindings = format(query);\n\n const cachedStatement = this.#stmts.cache.get(sqlAndBindings.text);\n cachedStatement.statement.safeIntegers(true);\n const rowIterator = cachedStatement.statement.iterate<Row>(\n ...sqlAndBindings.values,\n );\n try {\n debug?.initQuery(this.#table, sqlAndBindings.text);\n\n if (sort) {\n const comparator = makeComparator(sort, req.reverse);\n yield* generateWithStart(\n generateWithYields(\n generateWithOverlay(\n req.start?.row,\n this.#mapFromSQLiteTypes(\n this.#columns,\n rowIterator,\n sqlAndBindings.text,\n debug,\n ),\n req.constraint,\n this.#overlay,\n connection.lastPushedEpoch,\n comparator,\n connection.filters?.predicate,\n ),\n this.#shouldYield,\n ),\n req.start,\n comparator,\n );\n } else {\n yield* generateWithYields(\n generateWithOverlayUnordered(\n this.#mapFromSQLiteTypes(\n this.#columns,\n rowIterator,\n sqlAndBindings.text,\n debug,\n ),\n req.constraint,\n this.#overlay,\n connection.lastPushedEpoch,\n this.#primaryKey,\n connection.filters?.predicate,\n ),\n this.#shouldYield,\n );\n }\n } finally {\n // Ensure the SQLite iterate() is closed.\n rowIterator.return?.();\n if (debug) {\n let totalNvisit = 0;\n let i = 0;\n while (true) {\n const nvisit = cachedStatement.statement.scanStatus(\n i++,\n SQLite3Database.SQLITE_SCANSTAT_NVISIT,\n 1,\n );\n if (nvisit === undefined) {\n break;\n }\n totalNvisit += Number(nvisit);\n }\n if (totalNvisit !== 0) {\n debug.recordNVisit(this.#table, sqlAndBindings.text, totalNvisit);\n }\n cachedStatement.statement.scanStatusReset();\n }\n this.#stmts.cache.return(cachedStatement);\n }\n }\n\n *#mapFromSQLiteTypes(\n valueTypes: Record<string, SchemaValue>,\n rowIterator: IterableIterator<Row>,\n query: string,\n debug: DebugDelegate | undefined,\n ): IterableIterator<Row> {\n let result;\n do {\n result = timeSampled(\n this.#lc,\n ++eventCount,\n this.#logConfig.ivmSampling,\n () => rowIterator.next(),\n this.#logConfig.slowRowThreshold,\n () =>\n `table-source.next took too long for ${query}. Are you missing an index?`,\n );\n if (result.done) {\n break;\n }\n const row = fromSQLiteTypes(valueTypes, result.value, this.#table);\n debug?.rowVended(this.#table, query, row);\n yield row;\n } while (!result.done);\n }\n\n *push(change: SourceChange): Stream<'yield'> {\n for (const result of this.genPush(change)) {\n if (result === 'yield') {\n yield result;\n }\n }\n }\n\n *genPush(change: SourceChange) {\n const exists = (row: Row) =>\n this.#stmts.checkExists.get<{exists: number} | undefined>(\n ...toSQLiteTypes(this.#primaryKey, row, this.#columns),\n )?.exists === 1;\n const setOverlay = (o: Overlay | undefined) => (this.#overlay = o);\n const writeChange = (c: SourceChange) => this.#writeChange(c);\n\n yield* genPushAndWriteWithSplitEdit(\n this.#connections,\n change,\n exists,\n setOverlay,\n writeChange,\n () => ++this.#pushEpoch,\n );\n }\n\n #writeChange(change: SourceChange) {\n switch (change.type) {\n case 'add':\n this.#stmts.insert.run(\n ...toSQLiteTypes(\n Object.keys(this.#columns),\n change.row,\n this.#columns,\n ),\n );\n break;\n case 'remove':\n this.#stmts.delete.run(\n ...toSQLiteTypes(this.#primaryKey, change.row, this.#columns),\n );\n break;\n case 'edit': {\n // If the PK is the same, use UPDATE.\n if (\n canUseUpdate(\n change.oldRow,\n change.row,\n this.#columns,\n this.#primaryKey,\n )\n ) {\n const mergedRow = {\n ...change.oldRow,\n ...change.row,\n };\n const params = [\n ...nonPrimaryValues(this.#columns, this.#primaryKey, mergedRow),\n ...toSQLiteTypes(this.#primaryKey, mergedRow, this.#columns),\n ];\n must(this.#stmts.update).run(params);\n } else {\n this.#stmts.delete.run(\n ...toSQLiteTypes(this.#primaryKey, change.oldRow, this.#columns),\n );\n this.#stmts.insert.run(\n ...toSQLiteTypes(\n Object.keys(this.#columns),\n change.row,\n this.#columns,\n ),\n );\n }\n\n break;\n }\n default:\n unreachable(change);\n }\n }\n\n #getRowStmtCache = new Map<string, string>();\n\n #getRowStmt(keyCols: string[]): string {\n const keyString = JSON.stringify(keyCols);\n let stmt = this.#getRowStmtCache.get(keyString);\n if (!stmt) {\n stmt = compile(\n sql`SELECT ${this.#allColumns} FROM ${sql.ident(\n this.#table,\n )} WHERE ${sql.join(\n keyCols.map(k => sql`${sql.ident(k)}=?`),\n sql` AND`,\n )}`,\n );\n this.#getRowStmtCache.set(keyString, stmt);\n }\n return stmt;\n }\n\n /**\n * Retrieves a row from the backing DB by a unique key, or `undefined` if such a\n * row does not exist. This is not used in the IVM pipeline but is useful\n * for retrieving data that is consistent with the state (and type\n * semantics) of the pipeline. Note that this key may not necessarily correspond\n * to the `primaryKey` with which this TableSource.\n */\n getRow(rowKey: Row): Row | undefined {\n const keyCols = Object.keys(rowKey);\n\n const stmt = this.#getRowStmt(keyCols);\n const row = this.#stmts.cache.use(stmt, cached =>\n cached.statement\n .safeIntegers(true)\n .get<Row>(...toSQLiteTypes(keyCols, rowKey, this.#columns)),\n );\n if (row) {\n return fromSQLiteTypes(this.#columns, row, this.#table);\n }\n return row;\n }\n\n #requestToSQL(\n request: FetchRequest,\n filters: NoSubqueryCondition | undefined,\n order: Ordering | undefined,\n ): SQLQuery {\n return buildSelectQuery(\n this.#table,\n this.#columns,\n request.constraint,\n filters,\n order,\n request.reverse,\n request.start,\n );\n }\n}\n\nfunction getUniqueIndexes(\n db: Database,\n tableName: string,\n): Map<string, Set<string>> {\n const sqlAndBindings = format(\n sql`\n SELECT idx.name, json_group_array(col.name) as columnsJSON\n FROM sqlite_master as idx\n JOIN pragma_index_list(idx.tbl_name) AS info ON info.name = idx.name\n JOIN pragma_index_info(idx.name) as col\n WHERE idx.tbl_name = ${tableName} AND\n idx.type = 'index' AND \n info.\"unique\" != 0\n GROUP BY idx.name\n ORDER BY idx.name`,\n );\n const stmt = db.prepare(sqlAndBindings.text);\n const indexes = stmt.all<{columnsJSON: string}>(...sqlAndBindings.values);\n return new Map(\n indexes.map(({columnsJSON}) => {\n const columns = JSON.parse(columnsJSON);\n const set = new Set<string>(columns);\n return [JSON.stringify(columns.sort()), set];\n }),\n );\n}\n\nexport function toSQLiteTypes(\n columns: readonly string[],\n row: Row,\n columnTypes: Record<string, SchemaValue>,\n): readonly unknown[] {\n return columns.map(col => toSQLiteType(row[col], columnTypes[col].type));\n}\n\nexport function toSQLiteTypeName(type: ValueType) {\n switch (type) {\n case 'boolean':\n return 'INTEGER';\n case 'number':\n return 'REAL';\n case 'string':\n return 'TEXT';\n case 'null':\n return 'NULL';\n case 'json':\n return 'TEXT';\n }\n}\n\nexport function fromSQLiteTypes(\n valueTypes: Record<string, SchemaValue>,\n row: Row,\n tableName: string,\n): Row {\n const newRow: Writable<Row> = {};\n for (const key of Object.keys(row)) {\n const valueType = valueTypes[key];\n if (valueType === undefined) {\n const columnList = Object.keys(valueTypes).sort().join(', ');\n throw new Error(\n `Invalid column \"${key}\" for table \"${tableName}\". Synced columns include ${columnList}`,\n );\n }\n newRow[key] = fromSQLiteType(valueType.type, row[key], key, tableName);\n }\n return newRow;\n}\n\nfunction fromSQLiteType(\n valueType: ValueType,\n v: Value,\n column: string,\n tableName: string,\n): Value {\n if (v === null) {\n return null;\n }\n switch (valueType) {\n case 'boolean':\n return !!v;\n case 'number':\n case 'string':\n case 'null':\n if (typeof v === 'bigint') {\n if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {\n throw new UnsupportedValueError(\n `value ${v} (in ${tableName}.${column}) is outside of supported bounds`,\n );\n }\n return Number(v);\n }\n return v;\n case 'json':\n try {\n return JSON.parse(v as string);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n throw new UnsupportedValueError(\n `Failed to parse JSON for ${tableName}.${column}: ${errorMessage}`,\n {cause: error},\n );\n }\n }\n}\n\nexport class UnsupportedValueError extends Error {}\n\nfunction canUseUpdate(\n oldRow: Row,\n row: Row,\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n): boolean {\n for (const pk of primaryKey) {\n if (oldRow[pk] !== row[pk]) {\n return false;\n }\n }\n return Object.keys(columns).length > primaryKey.length;\n}\n\nfunction nonPrimaryValues(\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n row: Row,\n): Iterable<unknown> {\n return nonPrimaryKeys(columns, primaryKey).map(c =>\n toSQLiteType(row[c], columns[c].type),\n );\n}\n\nfunction nonPrimaryKeys(\n columns: Record<string, SchemaValue>,\n primaryKey: PrimaryKey,\n) {\n return Object.keys(columns).filter(c => !primaryKey.includes(c));\n}\n\nfunction* generateWithYields(stream: Stream<Node>, shouldYield: () => boolean) {\n for (const n of stream) {\n if (shouldYield()) {\n yield 'yield';\n }\n yield n;\n }\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.
|
|
3
|
+
"version": "1.3.0-canary.0",
|
|
4
4
|
"description": "Zero is a web framework for serverless web development.",
|
|
5
5
|
"author": "Rocicorp, Inc.",
|
|
6
6
|
"repository": {
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"@types/basic-auth": "^1.1.8",
|
|
52
52
|
"@types/ws": "^8.5.12",
|
|
53
53
|
"basic-auth": "^2.0.1",
|
|
54
|
-
"chalk": "^5.3.0",
|
|
55
54
|
"chalk-template": "^1.1.0",
|
|
56
55
|
"chokidar": "^4.0.1",
|
|
57
56
|
"cloudevents": "^10.0.0",
|
|
@@ -67,7 +66,7 @@
|
|
|
67
66
|
"json-custom-numbers": "^3.1.1",
|
|
68
67
|
"kasi": "^1.1.0",
|
|
69
68
|
"nanoid": "^5.1.2",
|
|
70
|
-
"oxfmt": "^0.
|
|
69
|
+
"oxfmt": "^0.44.0",
|
|
71
70
|
"parse-prometheus-text-format": "^1.1.1",
|
|
72
71
|
"pg-format": "npm:pg-format-fix@^1.0.5",
|
|
73
72
|
"postgres": "3.4.7",
|
|
@@ -79,18 +78,18 @@
|
|
|
79
78
|
},
|
|
80
79
|
"devDependencies": {
|
|
81
80
|
"@op-engineering/op-sqlite": ">=15",
|
|
82
|
-
"@vitest/runner": "4.1.
|
|
81
|
+
"@vitest/runner": "4.1.3",
|
|
83
82
|
"analyze-query": "0.0.0",
|
|
84
83
|
"ast-to-zql": "0.0.0",
|
|
85
84
|
"expo-sqlite": ">=15",
|
|
86
85
|
"replicache": "15.2.1",
|
|
87
86
|
"shared": "0.0.0",
|
|
88
|
-
"syncpack": "^14.
|
|
87
|
+
"syncpack": "^14.3.0",
|
|
89
88
|
"typedoc": "^0.28.17",
|
|
90
89
|
"typedoc-plugin-markdown": "^4.10.0",
|
|
91
|
-
"typescript": "~
|
|
90
|
+
"typescript": "~6.0.2",
|
|
92
91
|
"vite": "8.0.3",
|
|
93
|
-
"vitest": "4.1.
|
|
92
|
+
"vitest": "4.1.3",
|
|
94
93
|
"zero-cache": "0.0.0",
|
|
95
94
|
"zero-client": "0.0.0",
|
|
96
95
|
"zero-pg": "0.0.0",
|