@uwdata/mosaic-core
Advanced tools
Comparing version 0.11.0 to 0.12.0
{ | ||
"name": "@uwdata/mosaic-core", | ||
"version": "0.11.0", | ||
"version": "0.12.0", | ||
"description": "Scalable and extensible linked data views.", | ||
@@ -19,2 +19,3 @@ "keywords": [ | ||
"unpkg": "dist/mosaic-core.min.js", | ||
"types": "dist/types/index.d.ts", | ||
"repository": { | ||
@@ -26,16 +27,17 @@ "type": "git", | ||
"prebuild": "rimraf dist && mkdir dist", | ||
"build": "node ../../esbuild.js mosaic-core", | ||
"build": "npm run types && node ../../esbuild.js mosaic-core", | ||
"types": "tsc", | ||
"lint": "eslint src test", | ||
"test": "vitest run --dangerouslyIgnoreUnhandledErrors", | ||
"test": "vitest run && tsc -p jsconfig.json", | ||
"prepublishOnly": "npm run test && npm run lint && npm run build" | ||
}, | ||
"dependencies": { | ||
"@duckdb/duckdb-wasm": "^1.28.1-dev278.0", | ||
"@uwdata/flechette": "^1.0.2", | ||
"@uwdata/mosaic-sql": "^0.11.0" | ||
"@duckdb/duckdb-wasm": "^1.29.0", | ||
"@uwdata/flechette": "^1.1.1", | ||
"@uwdata/mosaic-sql": "^0.12.0" | ||
}, | ||
"devDependencies": { | ||
"@uwdata/mosaic-duckdb": "^0.11.0" | ||
"@uwdata/mosaic-duckdb": "^0.12.0" | ||
}, | ||
"gitHead": "861d616f39926a1d2aee83b59dbdd70b0b3caf12" | ||
"gitHead": "523b1afe2a0880291c92f81e4a7b91829362d285" | ||
} |
# mosaic-core | ||
The core Mosaic components: a central coordinator, parameters (`Param`) and selections (`Selection`) for linking scalar values or query predicates (respectively) across Mosaic clients, and filter groups with optimized index management. The Mosaic coordinator can send queries either over the network to a backing server (`socket` and `rest` clients) or to a client-side [DuckDB-WASM](https://github.com/duckdb/duckdb-wasm) instance (`wasm` client). | ||
The core Mosaic components: a central coordinator, parameters (`Param`) and selections (`Selection`) for linking scalar values or query predicates (respectively) across Mosaic clients, and filter groups with materialized views of pre-aggregated data. The Mosaic coordinator can send queries either over the network to a backing server (`socket` and `rest` clients) or to a client-side [DuckDB-WASM](https://github.com/duckdb/duckdb-wasm) instance (`wasm` client). | ||
The `mosaic-core` facilities are included as part of the [vgplot](https://github.com/uwdata/mosaic/tree/main/packages/vgplot) API. |
@@ -9,3 +9,5 @@ import { decodeIPC } from '../util/decode-ipc.js'; | ||
* @param {'exec' | 'arrow' | 'json' | 'create-bundle' | 'load-bundle'} [query.type] The query type. | ||
* @param {string} query.sql A SQL query string. | ||
* @param {string} [query.sql] A SQL query string. | ||
* @param {string[]} [query.queries] The queries used to create a bundle. | ||
* @param {string} [query.name] The name of a bundle to create or load. | ||
* @returns the query result | ||
@@ -12,0 +14,0 @@ */ |
@@ -89,3 +89,5 @@ import { decodeIPC } from '../util/decode-ipc.js'; | ||
* @param {'exec' | 'arrow' | 'json' | 'create-bundle' | 'load-bundle'} [query.type] The query type. | ||
* @param {string} query.sql A SQL query string. | ||
* @param {string} [query.sql] A SQL query string. | ||
* @param {string[]} [query.queries] The queries used to create a bundle. | ||
* @param {string} [query.name] The name of a bundle to create or load. | ||
* @returns the query result | ||
@@ -92,0 +94,0 @@ */ |
@@ -64,3 +64,3 @@ import * as duckdb from '@duckdb/duckdb-wasm'; | ||
* @param {object} query | ||
* @param {'exec' | 'arrow' | 'json' | 'create-bundle' | 'load-bundle'} [query.type] The query type. | ||
* @param {'exec' | 'arrow' | 'json'} [query.type] The query type. | ||
* @param {string} query.sql A SQL query string. | ||
@@ -67,0 +67,0 @@ * @returns the query result |
import { socketConnector } from './connectors/socket.js'; | ||
import { DataCubeIndexer } from './DataCubeIndexer.js'; | ||
import { MosaicClient } from './MosaicClient.js'; | ||
import { QueryManager, Priority } from './QueryManager.js'; | ||
import { PreAggregator } from './preagg/PreAggregator.js'; | ||
import { queryFieldInfo } from './util/field-info.js'; | ||
import { QueryResult } from './util/query-result.js'; | ||
import { voidLogger } from './util/void-logger.js'; | ||
import { MosaicClient } from './MosaicClient.js'; | ||
import { QueryManager, Priority } from './QueryManager.js'; | ||
@@ -36,3 +36,3 @@ /** | ||
* handles selection updates. The Coordinator also performs optimizations | ||
* including query caching, consolidation, and data cube indexing. | ||
* including query caching, consolidation, and pre-aggregation. | ||
* @param {*} [db] Database connector. Defaults to a web socket connection. | ||
@@ -44,4 +44,4 @@ * @param {object} [options] Coordinator options. | ||
* @param {boolean} [options.consolidate=true] Boolean flag to enable/disable query consolidation. | ||
* @param {import('./DataCubeIndexer.js').DataCubeIndexerOptions} [options.indexes] | ||
* Data cube indexer options. | ||
* @param {import('./preagg/PreAggregator.js').PreAggregateOptions} [options.preagg] | ||
* Options for the Pre-aggregator. | ||
*/ | ||
@@ -54,3 +54,3 @@ export class Coordinator { | ||
consolidate = true, | ||
indexes = {} | ||
preagg = {} | ||
} = {}) { | ||
@@ -64,3 +64,3 @@ /** @type {QueryManager} */ | ||
this.clear(); | ||
this.dataCubeIndexer = new DataCubeIndexer(this, indexes); | ||
this.preaggregator = new PreAggregator(this, preagg); | ||
} | ||
@@ -215,3 +215,3 @@ | ||
* the client is simply updated. Otherwise `updateClient` is called. As a | ||
* side effect, this method clears the current data cube indexer state. | ||
* side effect, this method clears the current preaggregator state. | ||
* @param {MosaicClient} client The client to update. | ||
@@ -221,3 +221,3 @@ * @param {QueryType | null} [query] The query to issue. | ||
requestQuery(client, query) { | ||
this.dataCubeIndexer.clear(); | ||
this.preaggregator.clear(); | ||
return query | ||
@@ -316,6 +316,6 @@ ? this.updateClient(client, query) | ||
function activateSelection(mc, selection, clause) { | ||
const { dataCubeIndexer, filterGroups } = mc; | ||
const { preaggregator, filterGroups } = mc; | ||
const { clients } = filterGroups.get(selection); | ||
for (const client of clients) { | ||
dataCubeIndexer.index(client, selection, clause); | ||
preaggregator.request(client, selection, clause); | ||
} | ||
@@ -332,7 +332,7 @@ } | ||
function updateSelection(mc, selection) { | ||
const { dataCubeIndexer, filterGroups } = mc; | ||
const { preaggregator, filterGroups } = mc; | ||
const { clients } = filterGroups.get(selection); | ||
const { active } = selection; | ||
return Promise.allSettled(Array.from(clients, client => { | ||
const info = dataCubeIndexer.index(client, selection, active); | ||
const info = preaggregator.request(client, selection, active); | ||
const filter = info ? null : selection.predicate(client); | ||
@@ -339,0 +339,0 @@ |
@@ -25,1 +25,14 @@ export { MosaicClient } from './MosaicClient.js'; | ||
export { toDataColumns } from './util/to-data-columns.js'; | ||
/** | ||
* @typedef {import('./util/selection-types.js').ClauseMetadata} ClauseMetadata | ||
* @typedef {import('./util/selection-types.js').PointMetadata} PointMetadata | ||
* @typedef {import('./util/selection-types.js').MatchMethod} MatchMethod | ||
* @typedef {import('./util/selection-types.js').MatchMetadata} MatchMetadata | ||
* @typedef {import('./util/selection-types.js').ScaleType} ScaleType | ||
* @typedef {import('./util/selection-types.js').Extent} Extent | ||
* @typedef {import('./util/selection-types.js').Scale} Scale | ||
* @typedef {import('./util/selection-types.js').BinMethod} BinMethod | ||
* @typedef {import('./util/selection-types.js').IntervalMetadata} IntervalMetadata | ||
* @typedef {import('./util/selection-types.js').SelectionClause} SelectionClause | ||
*/ |
@@ -41,7 +41,8 @@ import { throttle } from './util/throttle.js'; | ||
/** | ||
* Return a boolean indicating if the client query can be indexed. Should | ||
* return true if changes to the filterBy selection does not change the | ||
* groupby domain of the client query. | ||
* Return a boolean indicating if the client query can be sped up with | ||
* materialized views of pre-aggregated data. Should return true if changes to | ||
* the filterBy selection does not change the groupby domain of the client | ||
* query. | ||
*/ | ||
get filterIndexable() { | ||
get filterStable() { | ||
return true; | ||
@@ -48,0 +49,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { Query, Ref, isDescribeQuery } from '@uwdata/mosaic-sql'; | ||
import { DescribeQuery, isAggregateExpression, isColumnRef, isDescribeQuery, isSelectQuery, Query } from '@uwdata/mosaic-sql'; | ||
import { QueryResult } from './util/query-result.js'; | ||
@@ -16,6 +16,5 @@ | ||
* @param {*} cache Client-side query cache (sql -> data) | ||
* @param {*} record Query recorder function | ||
* @returns A consolidator object | ||
*/ | ||
export function consolidator(enqueue, cache, record) { | ||
export function consolidator(enqueue, cache) { | ||
let pending = []; | ||
@@ -32,3 +31,3 @@ let id = 0; | ||
for (const group of groups) { | ||
consolidate(group, enqueue, record); | ||
consolidate(group, enqueue); | ||
processResults(group, cache); | ||
@@ -80,3 +79,3 @@ } | ||
* which is indicated by returning the precise query SQL as the key. | ||
* @param {*} query The input query. | ||
* @param {Query | DescribeQuery} query The input query. | ||
* @param {*} cache The query cache (sql -> data). | ||
@@ -87,8 +86,6 @@ * @returns a key string | ||
const sql = `${query}`; | ||
if (query instanceof Query && !cache.get(sql)) { | ||
if (isSelectQuery(query) && !cache.get(sql)) { | ||
if ( | ||
// @ts-ignore | ||
query.orderby().length || query.where().length || | ||
// @ts-ignore | ||
query.qualify().length || query.having().length | ||
query._orderby.length || query._where.length || | ||
query._qualify.length || query._having.length | ||
) { | ||
@@ -101,3 +98,3 @@ // do not try to analyze if query includes clauses | ||
// create a derived query stripped of selections | ||
const q = query.clone().$select('*'); | ||
const q = query.clone().setSelect('*'); | ||
@@ -107,16 +104,12 @@ // check group by criteria for compatibility | ||
// we resolve these against the true grouping expressions | ||
const groupby = query.groupby(); | ||
// @ts-ignore | ||
const groupby = query._groupby; | ||
if (groupby.length) { | ||
const map = {}; // expression map (as -> expr) | ||
// @ts-ignore | ||
query.select().forEach(({ as, expr }) => map[as] = expr); | ||
// @ts-ignore | ||
q.$groupby(groupby.map(e => (e instanceof Ref && map[e.column]) || e)); | ||
const map = {}; // expression map (alias -> expr) | ||
query._select.forEach(({ alias, expr }) => map[alias] = expr); | ||
q.setGroupby(groupby.map(e => (isColumnRef(e) && map[e.column]) || e)); | ||
} | ||
// @ts-ignore | ||
else if (query.select().some(({ expr }) => expr.aggregate)) { | ||
else if (query._select.some(e => isAggregateExpression(e.expr))) { | ||
// if query is an ungrouped aggregate, add an explicit groupby to | ||
// prevent improper consolidation with non-aggregate queries | ||
q.$groupby('ALL'); | ||
q.setGroupby('ALL'); | ||
} | ||
@@ -136,5 +129,4 @@ | ||
* @param {*} enqueue Add entry to query queue | ||
* @param {*} record Query recorder function | ||
*/ | ||
function consolidate(group, enqueue, record) { | ||
function consolidate(group, enqueue) { | ||
if (shouldConsolidate(group)) { | ||
@@ -146,4 +138,3 @@ // issue a single consolidated query | ||
cache: false, | ||
record: false, | ||
query: (group.query = consolidatedQuery(group, record)) | ||
query: (group.query = consolidatedQuery(group)) | ||
}, | ||
@@ -181,6 +172,5 @@ result: (group.result = new QueryResult()) | ||
* @param {*} group Array of bundled query entries | ||
* @param {*} record Query recorder function | ||
* @returns A consolidated Query instance | ||
*/ | ||
function consolidatedQuery(group, record) { | ||
function consolidatedQuery(group) { | ||
const maps = group.maps = []; | ||
@@ -194,3 +184,3 @@ const fields = new Map; | ||
maps.push(fieldMap); | ||
for (const { as, expr } of query.select()) { | ||
for (const { alias, expr } of query._select) { | ||
const e = `${expr}`; | ||
@@ -201,5 +191,4 @@ if (!fields.has(e)) { | ||
const [name] = fields.get(e); | ||
fieldMap.push([name, as]); | ||
fieldMap.push([name, alias]); | ||
} | ||
record(`${query}`); | ||
} | ||
@@ -211,11 +200,11 @@ | ||
// update group by statement as needed | ||
const groupby = query.groupby(); | ||
const groupby = query._groupby; | ||
if (groupby.length) { | ||
const map = {}; | ||
group.maps[0].forEach(([name, as]) => map[as] = name); | ||
query.$groupby(groupby.map(e => (e instanceof Ref && map[e.column]) || e)); | ||
query.setGroupby(groupby.map(e => (isColumnRef(e) && map[e.column]) || e)); | ||
} | ||
// update select statement and return | ||
return query.$select(Array.from(fields.values())); | ||
return query.setSelect(Array.from(fields.values())); | ||
} | ||
@@ -222,0 +211,0 @@ |
import { consolidator } from './QueryConsolidator.js'; | ||
import { lruCache, voidCache } from './util/cache.js'; | ||
import { PriorityQueue } from './util/priority-queue.js'; | ||
import { QueryResult } from './util/query-result.js'; | ||
import { QueryResult, QueryState } from './util/query-result.js'; | ||
import { voidLogger } from './util/void-logger.js'; | ||
export const Priority = { High: 0, Normal: 1, Low: 2 }; | ||
export const Priority = Object.freeze({ High: 0, Normal: 1, Low: 2 }); | ||
export class QueryManager { | ||
constructor() { | ||
constructor( | ||
maxConcurrentRequests = 32 | ||
) { | ||
this.queue = new PriorityQueue(3); | ||
this.db = null; | ||
this.clientCache = null; | ||
this._logger = null; | ||
this._logger = voidLogger(); | ||
this._logQueries = false; | ||
this.recorders = []; | ||
this.pending = null; | ||
this._consolidate = null; | ||
/** | ||
* Requests pending with the query manager. | ||
* | ||
* @type {QueryResult[]} | ||
*/ | ||
this.pendingResults = []; | ||
this.maxConcurrentRequests = maxConcurrentRequests; | ||
this.pendingExec = false; | ||
} | ||
next() { | ||
if (this.pending || this.queue.isEmpty()) return; | ||
if (this.queue.isEmpty() || this.pendingResults.length > this.maxConcurrentRequests || this.pendingExec) { | ||
return; | ||
} | ||
const { request, result } = this.queue.next(); | ||
this.pending = this.submit(request, result); | ||
this.pending.finally(() => { this.pending = null; this.next(); }); | ||
this.pendingResults.push(result); | ||
if (request.type === 'exec') this.pendingExec = true; | ||
this.submit(request, result).finally(() => { | ||
// return from the queue all requests that are ready | ||
while (this.pendingResults.length && this.pendingResults[0].state !== QueryState.pending) { | ||
const result = this.pendingResults.shift(); | ||
if (result.state === QueryState.ready) { | ||
result.fulfill(); | ||
} else if (result.state === QueryState.done) { | ||
this._logger.warn('Found resolved query in pending results.'); | ||
} | ||
} | ||
if (request.type === 'exec') this.pendingExec = false; | ||
this.next(); | ||
}); | ||
} | ||
/** | ||
* Add an entry to the query queue with a priority. | ||
* @param {object} entry The entry to add. | ||
* @param {*} [entry.request] The query request. | ||
* @param {QueryResult} [entry.result] The query result. | ||
* @param {number} priority The query priority, defaults to `Priority.Normal`. | ||
*/ | ||
enqueue(entry, priority = Priority.Normal) { | ||
@@ -32,18 +66,12 @@ this.queue.insert(entry, priority); | ||
recordQuery(sql) { | ||
if (this.recorders.length && sql) { | ||
this.recorders.forEach(rec => rec.add(sql)); | ||
} | ||
} | ||
/** | ||
* Submit the query to the connector. | ||
* @param {*} request The request. | ||
* @param {QueryResult} result The query result. | ||
*/ | ||
async submit(request, result) { | ||
try { | ||
const { query, type, cache = false, record = true, options } = request; | ||
const { query, type, cache = false, options } = request; | ||
const sql = query ? `${query}` : null; | ||
// update recorders | ||
if (record) { | ||
this.recordQuery(sql); | ||
} | ||
// check query cache | ||
@@ -53,4 +81,5 @@ if (cache) { | ||
if (cached) { | ||
const data = await cached; | ||
this._logger.debug('Cache'); | ||
result.fulfill(cached); | ||
result.ready(data); | ||
return; | ||
@@ -65,6 +94,12 @@ } | ||
} | ||
const data = await this.db.query({ type, sql, ...options }); | ||
const promise = this.db.query({ type, sql, ...options }); | ||
if (cache) this.clientCache.set(sql, promise); | ||
const data = await promise; | ||
if (cache) this.clientCache.set(sql, data); | ||
this._logger.debug(`Request: ${(performance.now() - t0).toFixed(1)}`); | ||
result.fulfill(data); | ||
result.ready(type === 'exec' ? null : data); | ||
} catch (err) { | ||
@@ -95,3 +130,3 @@ result.reject(err); | ||
if (flag && !this._consolidate) { | ||
this._consolidate = consolidator(this.enqueue.bind(this), this.clientCache, this.recordQuery.bind(this)); | ||
this._consolidate = consolidator(this.enqueue.bind(this), this.clientCache); | ||
} else if (!flag && this._consolidate) { | ||
@@ -102,2 +137,8 @@ this._consolidate = null; | ||
/** | ||
* Request a query result. | ||
* @param {*} request The request. | ||
* @param {number} priority The query priority, defaults to `Priority.Normal`. | ||
* @returns {QueryResult} A query result promise. | ||
*/ | ||
request(request, priority = Priority.Normal) { | ||
@@ -124,2 +165,8 @@ const result = new QueryResult(); | ||
}); | ||
for (const result of this.pendingResults) { | ||
if (set.has(result)) { | ||
result.reject('Canceled'); | ||
} | ||
} | ||
} | ||
@@ -133,24 +180,8 @@ } | ||
}); | ||
} | ||
record() { | ||
let state = []; | ||
const recorder = { | ||
add(query) { | ||
state.push(query); | ||
}, | ||
reset() { | ||
state = []; | ||
}, | ||
snapshot() { | ||
return state.slice(); | ||
}, | ||
stop() { | ||
this.recorders = this.recorders.filter(x => x !== recorder); | ||
return state; | ||
} | ||
}; | ||
this.recorders.push(recorder); | ||
return recorder; | ||
for (const result of this.pendingResults) { | ||
result.reject('Cleared'); | ||
} | ||
this.pendingResults = []; | ||
} | ||
} |
@@ -1,3 +0,4 @@ | ||
import { or } from '@uwdata/mosaic-sql'; | ||
import { literal, or } from '@uwdata/mosaic-sql'; | ||
import { Param } from './Param.js'; | ||
import { MosaicClient } from './MosaicClient.js'; | ||
@@ -318,5 +319,7 @@ /** | ||
* Return a selection query predicate for the given client. | ||
* @param {*[]} clauseList An array of selection clauses. | ||
* @param {*} active The current active selection clause. | ||
* @param {*} client The client whose data may be filtered. | ||
* @param {import('./util/selection-types.js').SelectionClause[]} clauseList | ||
* An array of selection clauses. | ||
* @param {import('./util/selection-types.js').SelectionClause} active | ||
* The current active selection clause. | ||
* @param {MosaicClient} client The client whose data may be filtered. | ||
* @returns {*} The query predicate for filtering client data, | ||
@@ -329,3 +332,3 @@ * based on the current state of this selection. | ||
if (empty && !clauseList.length) { | ||
return ['FALSE']; | ||
return [literal(false)]; | ||
} | ||
@@ -332,0 +335,0 @@ |
@@ -1,5 +0,2 @@ | ||
import { | ||
SQLExpression, and, contains, isBetween, isNotDistinct, literal, | ||
or, prefix, regexp_matches, suffix | ||
} from '@uwdata/mosaic-sql'; | ||
import { ExprNode, and, contains, isBetween, isIn, isNotDistinct, literal, or, prefix, regexp_matches, suffix } from '@uwdata/mosaic-sql'; | ||
import { MosaicClient } from './MosaicClient.js'; | ||
@@ -13,3 +10,2 @@ | ||
* @typedef {import('./util/selection-types.js').BinMethod} BinMethod | ||
* @typedef {SQLExpression | string} Field | ||
*/ | ||
@@ -19,3 +15,3 @@ | ||
* Generate a selection clause for a single selected point value. | ||
* @param {Field} field The table column or expression to select. | ||
* @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select. | ||
* @param {*} value The selected value. | ||
@@ -33,3 +29,3 @@ * @param {object} options Additional clause properties. | ||
}) { | ||
/** @type {SQLExpression | null} */ | ||
/** @type {ExprNode | null} */ | ||
const predicate = value !== undefined | ||
@@ -49,5 +45,5 @@ ? isNotDistinct(field, literal(value)) | ||
* Generate a selection clause for multiple selected point values. | ||
* @param {Field[]} fields The table columns or expressions to select. | ||
* @param {any[][]} value The selected values, as an array of arrays where | ||
* each subarray contains values corresponding to each *fields* entry. | ||
* @param {import('@uwdata/mosaic-sql').ExprValue[]} fields The table columns or expressions to select. | ||
* @param {any[][] | undefined} value The selected values, as an array of | ||
* arrays. Each subarray contains values for each *fields* entry. | ||
* @param {object} options Additional clause properties. | ||
@@ -64,10 +60,11 @@ * @param {*} options.source The source component generating this clause. | ||
}) { | ||
/** @type {SQLExpression | null} */ | ||
/** @type {ExprNode | null} */ | ||
let predicate = null; | ||
if (value) { | ||
const clauses = value.map(vals => { | ||
const list = vals.map((v, i) => isNotDistinct(fields[i], literal(v))); | ||
return list.length > 1 ? and(list) : list[0]; | ||
}); | ||
predicate = clauses.length > 1 ? or(clauses) : clauses[0]; | ||
const clauses = value.length && fields.length === 1 | ||
? [isIn(fields[0], value.map(v => literal(v[0])))] | ||
: value.map(v => and(v.map((_, i) => isNotDistinct(fields[i], literal(_))))); | ||
predicate = value.length === 0 ? literal(false) | ||
: clauses.length > 1 ? or(clauses) | ||
: clauses[0]; | ||
} | ||
@@ -85,3 +82,3 @@ return { | ||
* Generate a selection clause for a selected 1D interval. | ||
* @param {Field} field The table column or expression to select. | ||
* @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select. | ||
* @param {Extent} value The selected interval as a [lo, hi] array. | ||
@@ -105,3 +102,3 @@ * @param {object} options Additional clause properties. | ||
}) { | ||
/** @type {SQLExpression | null} */ | ||
/** @type {ExprNode | null} */ | ||
const predicate = value != null ? isBetween(field, value) : null; | ||
@@ -115,3 +112,3 @@ /** @type {import('./util/selection-types.js').IntervalMetadata} */ | ||
* Generate a selection clause for multiple selected intervals. | ||
* @param {Field[]} fields The table columns or expressions to select. | ||
* @param {import('@uwdata/mosaic-sql').ExprValue[]} fields The table columns or expressions to select. | ||
* @param {Extent[]} value The selected intervals, as an array of extents. | ||
@@ -136,3 +133,3 @@ * @param {object} options Additional clause properties. | ||
}) { | ||
/** @type {SQLExpression | null} */ | ||
/** @type {ExprNode | null} */ | ||
const predicate = value != null | ||
@@ -150,3 +147,3 @@ ? and(fields.map((f, i) => isBetween(f, value[i]))) | ||
* Generate a selection clause for text search matching. | ||
* @param {Field} field The table column or expression to select. | ||
* @param {import('@uwdata/mosaic-sql').ExprValue} field The table column or expression to select. | ||
* @param {string} value The selected text search query string. | ||
@@ -166,3 +163,3 @@ * @param {object} options Additional clause properties. | ||
let fn = MATCH_METHODS[method]; | ||
/** @type {SQLExpression | null} */ | ||
/** @type {ExprNode | null} */ | ||
const predicate = value ? fn(field, literal(value)) : null; | ||
@@ -169,0 +166,0 @@ /** @type {import('./util/selection-types.js').MatchMetadata} */ |
@@ -1,2 +0,2 @@ | ||
import { Query, asRelation, count, isNull, max, min, sql } from '@uwdata/mosaic-sql'; | ||
import { AggregateNode, Query, asTableRef, count, isNull, max, min, sql } from '@uwdata/mosaic-sql'; | ||
import { jsType } from './js-type.js'; | ||
@@ -11,2 +11,5 @@ | ||
/** | ||
* @type {Record<string, (column: string) => AggregateNode>} | ||
*/ | ||
const statMap = { | ||
@@ -20,10 +23,17 @@ [Count]: count, | ||
/** | ||
* | ||
* @param {string} table | ||
* @param {string} column | ||
* @param {string[]|Set<string>} stats | ||
* @returns | ||
*/ | ||
function summarize(table, column, stats) { | ||
return Query | ||
.from(table) | ||
.select(Array.from(stats, s => [s, statMap[s](column)])); | ||
.select(Array.from(stats, s => ({[s]: statMap[s](column)}))); | ||
} | ||
export async function queryFieldInfo(mc, fields) { | ||
if (fields.length === 1 && `${fields[0].column}` === '*') { | ||
if (fields.length === 1 && fields[0].column === '*') { | ||
return getTableInfo(mc, fields[0].table); | ||
@@ -40,3 +50,4 @@ } else { | ||
// use GROUP BY ALL to differentiate & consolidate aggregates | ||
const q = Query.from({ source: table }) | ||
const q = Query | ||
.from({ source: table }) | ||
.select({ column }) | ||
@@ -67,3 +78,3 @@ .groupby(column.aggregate ? sql`ALL` : []); | ||
async function getTableInfo(mc, table) { | ||
const result = await mc.query(`DESCRIBE ${asRelation(table)}`); | ||
const result = await mc.query(`DESCRIBE ${asTableRef(table)}`); | ||
return Array.from(result).map(desc => ({ | ||
@@ -70,0 +81,0 @@ table, |
@@ -10,3 +10,3 @@ // Fowler/Noll/Vo hashing. | ||
} | ||
return fnv_mix(a); | ||
return fnv_mix(a) >>> 0; // ensure non-zero value | ||
} | ||
@@ -13,0 +13,0 @@ |
@@ -0,1 +1,8 @@ | ||
export const QueryState = Object.freeze({ | ||
pending: Symbol('pending'), | ||
ready: Symbol('ready'), | ||
error: Symbol('error'), | ||
done: Symbol('done') | ||
}); | ||
/** | ||
@@ -18,6 +25,9 @@ * A query result Promise that can allows external callers | ||
this._reject = reject; | ||
this._state = QueryState.pending; | ||
this._value = undefined; | ||
} | ||
/** | ||
* Resolve the result Promise with the provided value. | ||
* Resolve the result Promise with a prepared value or the provided value. | ||
* This method will only succeed if either a value is provided or the promise is ready. | ||
* @param {*} value The result value. | ||
@@ -27,3 +37,15 @@ * @returns {this} | ||
fulfill(value) { | ||
this._resolve(value); | ||
if (this._value !== undefined) { | ||
if (value !== undefined) { | ||
throw Error('Promise is ready and fulfill has a provided value'); | ||
} | ||
this._resolve(this._value); | ||
} else if (value === undefined) { | ||
throw Error('Promise is neither ready nor has provided value'); | ||
} else { | ||
this._resolve(value); | ||
} | ||
this._state = QueryState.done; | ||
return this; | ||
@@ -33,2 +55,13 @@ } | ||
/** | ||
* Prepare to resolve with the provided value. | ||
* @param {*} value The result value. | ||
* @returns {this} | ||
*/ | ||
ready(value) { | ||
this._state = QueryState.ready; | ||
this._value = value; | ||
return this; | ||
} | ||
/** | ||
* Rejects the result Promise with the provided error. | ||
@@ -39,5 +72,14 @@ * @param {*} error The error value. | ||
reject(error) { | ||
this._state = QueryState.error; | ||
this._reject(error); | ||
return this; | ||
} | ||
/** | ||
* Returns the state of this query result. | ||
* @returns {symbol} | ||
*/ | ||
get state() { | ||
return this._state; | ||
} | ||
} | ||
@@ -44,0 +86,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { SQLExpression } from '@uwdata/mosaic-sql'; | ||
import { ExprNode } from '@uwdata/mosaic-sql'; | ||
import { MosaicClient } from '../MosaicClient.js'; | ||
@@ -130,9 +130,9 @@ | ||
*/ | ||
predicate: SQLExpression | null; | ||
predicate: ExprNode | null; | ||
/** | ||
* Optional clause metadata that varies based on the selection type. | ||
* The metadata can be used to optimize selection queries, for example | ||
* by creating pre-aggregated data cubes when applicable. | ||
* by creating materialized views of pre-aggregated data when applicable. | ||
*/ | ||
meta?: ClauseMetadata; | ||
} |
@@ -19,11 +19,13 @@ const NIL = {}; | ||
function invoke(event) { | ||
curr = callback(event).finally(() => { | ||
if (next) { | ||
const { value } = next; | ||
next = null; | ||
invoke(value); | ||
} else { | ||
curr = null; | ||
} | ||
}); | ||
curr = callback(event) | ||
.catch(() => {}) | ||
.finally(() => { | ||
if (next) { | ||
const { value } = next; | ||
next = null; | ||
invoke(value); | ||
} else { | ||
curr = null; | ||
} | ||
}); | ||
} | ||
@@ -30,0 +32,0 @@ |
@@ -0,9 +1,10 @@ | ||
/* eslint-disable no-unused-vars */ | ||
export function voidLogger() { | ||
return { | ||
debug() {}, | ||
info() {}, | ||
log() {}, | ||
warn() {}, | ||
error() {} | ||
debug(..._) {}, | ||
info(..._) {}, | ||
log(..._) {}, | ||
warn(..._) {}, | ||
error(..._) {} | ||
}; | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1082290
65
24453
+ Added@uwdata/mosaic-sql@0.12.1(transitive)
- Removed@uwdata/mosaic-sql@0.11.0(transitive)
Updated@duckdb/duckdb-wasm@^1.29.0
Updated@uwdata/flechette@^1.1.1
Updated@uwdata/mosaic-sql@^0.12.0