@v587d/capital-generation 0.0.0-stage → 2.6.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/CHANGELOG.md +794 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/assets/cg-icon.png +0 -0
- package/capital-config/client.js +28 -0
- package/capital-config/client.src.cjs +388 -0
- package/capital-config/index.js +113 -0
- package/capital-config/package.json +36 -0
- package/capital-watchlist/client.js +182 -0
- package/capital-watchlist/client.src.cjs +1229 -0
- package/capital-watchlist/index.js +772 -0
- package/capital-watchlist/package.json +37 -0
- package/chart-ui/README.md +112 -0
- package/chart-ui/client.js +16 -0
- package/chart-ui/client.src.cjs +126 -0
- package/chart-ui/index.js +205 -0
- package/chart-ui/package.json +30 -0
- package/cordis.patch.yml +72 -0
- package/lib/agents/bash-guard.d.ts +55 -0
- package/lib/agents/bash-guard.js +162 -0
- package/lib/agents/root-tool-policy.d.ts +164 -0
- package/lib/agents/root-tool-policy.js +309 -0
- package/lib/chart/artifact-ref.d.ts +65 -0
- package/lib/chart/artifact-ref.js +107 -0
- package/lib/chart/downsample.d.ts +8 -0
- package/lib/chart/downsample.js +53 -0
- package/lib/chart/errors.d.ts +16 -0
- package/lib/chart/errors.js +15 -0
- package/lib/chart/events.d.ts +120 -0
- package/lib/chart/events.js +471 -0
- package/lib/chart/html.d.ts +21 -0
- package/lib/chart/html.js +154 -0
- package/lib/chart/runtime.d.ts +20 -0
- package/lib/chart/runtime.js +270 -0
- package/lib/chart/series.d.ts +102 -0
- package/lib/chart/series.js +424 -0
- package/lib/chart/source-token.d.ts +64 -0
- package/lib/chart/source-token.js +217 -0
- package/lib/chart/spec.d.ts +58 -0
- package/lib/chart/spec.js +265 -0
- package/lib/chart/tool.d.ts +86 -0
- package/lib/chart/tool.js +288 -0
- package/lib/chart/vendor/LICENSE +201 -0
- package/lib/chart/vendor/VERSION.json +8 -0
- package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
- package/lib/data-collector/args.d.ts +38 -0
- package/lib/data-collector/args.js +114 -0
- package/lib/data-collector/dataset-tools.d.ts +3 -0
- package/lib/data-collector/dataset-tools.js +421 -0
- package/lib/data-collector/describe.d.ts +99 -0
- package/lib/data-collector/describe.js +256 -0
- package/lib/data-collector/hub.d.ts +187 -0
- package/lib/data-collector/hub.js +469 -0
- package/lib/data-collector/query-time.d.ts +58 -0
- package/lib/data-collector/query-time.js +101 -0
- package/lib/data-collector/query.d.ts +57 -0
- package/lib/data-collector/query.js +473 -0
- package/lib/data-collector/store.d.ts +499 -0
- package/lib/data-collector/store.js +1486 -0
- package/lib/data-collector/time-axis.d.ts +169 -0
- package/lib/data-collector/time-axis.js +419 -0
- package/lib/data-collector/tools.d.ts +32 -0
- package/lib/data-collector/tools.js +216 -0
- package/lib/index.d.ts +275 -0
- package/lib/index.js +458 -0
- package/lib/net/eastmoney-client.d.ts +104 -0
- package/lib/net/eastmoney-client.js +118 -0
- package/lib/net/throttle.d.ts +16 -0
- package/lib/net/throttle.js +34 -0
- package/lib/ocr/client.d.ts +82 -0
- package/lib/ocr/client.js +281 -0
- package/lib/ocr/markdown.d.ts +75 -0
- package/lib/ocr/markdown.js +209 -0
- package/lib/ocr/store.d.ts +119 -0
- package/lib/ocr/store.js +226 -0
- package/lib/sources/eastmoney-http.d.ts +2 -0
- package/lib/sources/eastmoney-http.js +1253 -0
- package/lib/sources/fuyao-core.d.ts +52 -0
- package/lib/sources/fuyao-core.js +96 -0
- package/lib/sources/fuyao-rest.d.ts +14 -0
- package/lib/sources/fuyao-rest.js +2057 -0
- package/lib/sources/security-code.d.ts +27 -0
- package/lib/sources/security-code.js +104 -0
- package/lib/sources/security-identity.d.ts +24 -0
- package/lib/sources/security-identity.js +89 -0
- package/lib/sources/tencent-http.d.ts +2 -0
- package/lib/sources/tencent-http.js +1053 -0
- package/lib/sources/wind-mcp.d.ts +8 -0
- package/lib/sources/wind-mcp.js +593 -0
- package/lib/time/tools.d.ts +74 -0
- package/lib/time/tools.js +464 -0
- package/lib/tool-exec.d.ts +29 -0
- package/lib/tool-exec.js +17 -0
- package/lib/web-retriever/engines.d.ts +61 -0
- package/lib/web-retriever/engines.js +202 -0
- package/lib/web-retriever/html-markdown.d.ts +50 -0
- package/lib/web-retriever/html-markdown.js +321 -0
- package/lib/web-retriever/local-fetch.d.ts +121 -0
- package/lib/web-retriever/local-fetch.js +562 -0
- package/lib/web-retriever/retriever.d.ts +58 -0
- package/lib/web-retriever/retriever.js +94 -0
- package/lib/web-retriever/sources.d.ts +235 -0
- package/lib/web-retriever/sources.js +961 -0
- package/lib/web-retriever/tools.d.ts +50 -0
- package/lib/web-retriever/tools.js +1029 -0
- package/lib/web-retriever/wind-client.d.ts +69 -0
- package/lib/web-retriever/wind-client.js +311 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -4
- package/preset/capital-generation/README.md +161 -0
- package/preset/capital-generation/agent.patch.yml +509 -0
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export declare const MAX_QUERY_FILTERS = 32;
|
|
2
|
+
export declare const MAX_QUERY_GROUP_BY = 16;
|
|
3
|
+
export declare const MAX_QUERY_AGGREGATES = 16;
|
|
4
|
+
export declare const MAX_QUERY_SELECT = 32;
|
|
5
|
+
export declare const MAX_QUERY_ORDER_BY = 16;
|
|
6
|
+
export declare const MAX_QUERY_LIMIT = 200;
|
|
7
|
+
export declare const DEFAULT_QUERY_LIMIT = 100;
|
|
8
|
+
export declare const MAX_QUERY_GROUPS = 1000;
|
|
9
|
+
export declare const MAX_QUERY_OUTPUT_BYTES: number;
|
|
10
|
+
export declare const MAX_QUERY_SPEC_BYTES: number;
|
|
11
|
+
export type QueryErrorPolicy = 'strict' | 'skip_with_warning';
|
|
12
|
+
export type QueryOperator = '=' | '!=' | '>' | '>=' | '<' | '<=' | 'in' | 'is_null' | 'not_null';
|
|
13
|
+
export type QueryAggregateFunction = 'count' | 'min' | 'max' | 'avg' | 'sum';
|
|
14
|
+
export type QueryOrderDirection = 'asc' | 'desc';
|
|
15
|
+
export interface QueryFilter {
|
|
16
|
+
column: string;
|
|
17
|
+
operator: QueryOperator;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface QueryAggregate {
|
|
21
|
+
function: QueryAggregateFunction;
|
|
22
|
+
column?: string;
|
|
23
|
+
as: string;
|
|
24
|
+
}
|
|
25
|
+
export interface QueryOrderBy {
|
|
26
|
+
column: string;
|
|
27
|
+
direction: QueryOrderDirection;
|
|
28
|
+
}
|
|
29
|
+
export interface QuerySpec {
|
|
30
|
+
dataset_id: string;
|
|
31
|
+
select?: string[];
|
|
32
|
+
filters?: QueryFilter[];
|
|
33
|
+
group_by?: string[];
|
|
34
|
+
aggregates?: QueryAggregate[];
|
|
35
|
+
order_by?: QueryOrderBy[];
|
|
36
|
+
limit?: number;
|
|
37
|
+
error_policy?: QueryErrorPolicy;
|
|
38
|
+
}
|
|
39
|
+
export interface QueryResult {
|
|
40
|
+
dataset_id: string;
|
|
41
|
+
columns: string[];
|
|
42
|
+
rows: Array<Record<string, unknown>>;
|
|
43
|
+
matched_row_count: number;
|
|
44
|
+
group_count: number;
|
|
45
|
+
returned_count: number;
|
|
46
|
+
limit: number;
|
|
47
|
+
warnings: string[];
|
|
48
|
+
}
|
|
49
|
+
export type QueryErrorCode = 'query_spec_invalid' | 'query_column_not_found' | 'query_type_conflict' | 'query_group_limit_exceeded' | 'query_result_too_large' | 'query_rows_invalid';
|
|
50
|
+
export declare class DatasetQueryError extends Error {
|
|
51
|
+
readonly code: QueryErrorCode;
|
|
52
|
+
constructor(code: QueryErrorCode, detail: string);
|
|
53
|
+
}
|
|
54
|
+
export declare class JsonRowsQueryEngine {
|
|
55
|
+
execute(rows: unknown[], schema: unknown, input: unknown): QueryResult;
|
|
56
|
+
}
|
|
57
|
+
export declare function executeJsonRowsQuery(rows: unknown[], schema: unknown, query: unknown): QueryResult;
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
export const MAX_QUERY_FILTERS = 32;
|
|
2
|
+
export const MAX_QUERY_GROUP_BY = 16;
|
|
3
|
+
export const MAX_QUERY_AGGREGATES = 16;
|
|
4
|
+
export const MAX_QUERY_SELECT = 32;
|
|
5
|
+
export const MAX_QUERY_ORDER_BY = 16;
|
|
6
|
+
export const MAX_QUERY_LIMIT = 200;
|
|
7
|
+
export const DEFAULT_QUERY_LIMIT = 100;
|
|
8
|
+
export const MAX_QUERY_GROUPS = 1_000;
|
|
9
|
+
export const MAX_QUERY_OUTPUT_BYTES = 256 * 1024;
|
|
10
|
+
export const MAX_QUERY_SPEC_BYTES = 64 * 1024;
|
|
11
|
+
export class DatasetQueryError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, detail) {
|
|
14
|
+
super(`${code}: ${detail}`);
|
|
15
|
+
this.name = 'DatasetQueryError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function fail(code, detail) {
|
|
23
|
+
throw new DatasetQueryError(code, detail);
|
|
24
|
+
}
|
|
25
|
+
function validName(value) {
|
|
26
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 128;
|
|
27
|
+
}
|
|
28
|
+
function byteLength(value) {
|
|
29
|
+
return new TextEncoder().encode(value).byteLength;
|
|
30
|
+
}
|
|
31
|
+
function own(value, key) {
|
|
32
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
33
|
+
}
|
|
34
|
+
function rowCell(row, column) {
|
|
35
|
+
if (isRecord(row))
|
|
36
|
+
return { present: own(row, column), value: row[column] };
|
|
37
|
+
if (column === 'value')
|
|
38
|
+
return { present: true, value: row };
|
|
39
|
+
return { present: false, value: undefined };
|
|
40
|
+
}
|
|
41
|
+
function profileContractType(schema) {
|
|
42
|
+
if (!isRecord(schema))
|
|
43
|
+
return undefined;
|
|
44
|
+
if (schema.type === 'string')
|
|
45
|
+
return 'string';
|
|
46
|
+
if (schema.type === 'number')
|
|
47
|
+
return 'number';
|
|
48
|
+
if (schema.type === 'integer')
|
|
49
|
+
return 'integer';
|
|
50
|
+
if (schema.type === 'boolean')
|
|
51
|
+
return 'boolean';
|
|
52
|
+
if (schema.type === 'object')
|
|
53
|
+
return 'object';
|
|
54
|
+
if (schema.type === 'array')
|
|
55
|
+
return 'array';
|
|
56
|
+
if (Array.isArray(schema.oneOf)) {
|
|
57
|
+
for (const option of schema.oneOf) {
|
|
58
|
+
const type = profileContractType(option);
|
|
59
|
+
if (type)
|
|
60
|
+
return type;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return 'json';
|
|
64
|
+
}
|
|
65
|
+
function extractContracts(schema) {
|
|
66
|
+
if (!isRecord(schema))
|
|
67
|
+
return {};
|
|
68
|
+
let rowSchema = schema;
|
|
69
|
+
const rootProperties = isRecord(schema.properties) ? schema.properties : undefined;
|
|
70
|
+
const itemSchema = rootProperties && isRecord(rootProperties.item) ? rootProperties.item : undefined;
|
|
71
|
+
if (itemSchema)
|
|
72
|
+
rowSchema = itemSchema;
|
|
73
|
+
if (isRecord(rowSchema) && rowSchema.type === 'array' && rowSchema.items !== undefined)
|
|
74
|
+
rowSchema = rowSchema.items;
|
|
75
|
+
const properties = isRecord(rowSchema) && isRecord(rowSchema.properties) ? rowSchema.properties : undefined;
|
|
76
|
+
if (!properties)
|
|
77
|
+
return {};
|
|
78
|
+
const contracts = {};
|
|
79
|
+
for (const [column, property] of Object.entries(properties)) {
|
|
80
|
+
const type = profileContractType(property);
|
|
81
|
+
if (type)
|
|
82
|
+
contracts[column] = { type, allowNumericString: type === 'number' || type === 'integer' };
|
|
83
|
+
}
|
|
84
|
+
return contracts;
|
|
85
|
+
}
|
|
86
|
+
function observedType(value) {
|
|
87
|
+
if (value === null)
|
|
88
|
+
return 'null';
|
|
89
|
+
if (typeof value === 'number')
|
|
90
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
91
|
+
if (typeof value === 'boolean')
|
|
92
|
+
return 'boolean';
|
|
93
|
+
if (typeof value === 'string')
|
|
94
|
+
return 'string';
|
|
95
|
+
if (Array.isArray(value))
|
|
96
|
+
return 'array';
|
|
97
|
+
return 'object';
|
|
98
|
+
}
|
|
99
|
+
function numericString(value) {
|
|
100
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
const number = Number(value);
|
|
103
|
+
return Number.isFinite(number) ? number : undefined;
|
|
104
|
+
}
|
|
105
|
+
function numericValue(value, contract) {
|
|
106
|
+
const number = typeof value === 'number' && Number.isFinite(value)
|
|
107
|
+
? value
|
|
108
|
+
: contract?.allowNumericString
|
|
109
|
+
? numericString(value)
|
|
110
|
+
: undefined;
|
|
111
|
+
if (number === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
if (contract?.type === 'integer' && !Number.isSafeInteger(number))
|
|
114
|
+
return undefined;
|
|
115
|
+
return number;
|
|
116
|
+
}
|
|
117
|
+
function stableValue(value) {
|
|
118
|
+
if (value === null || typeof value !== 'object')
|
|
119
|
+
return JSON.stringify(value);
|
|
120
|
+
if (Array.isArray(value))
|
|
121
|
+
return `[${value.map(stableValue).join(',')}]`;
|
|
122
|
+
const record = value;
|
|
123
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableValue(record[key])}`).join(',')}}`;
|
|
124
|
+
}
|
|
125
|
+
function sameType(left, right) {
|
|
126
|
+
if (typeof left === 'number' && Number.isFinite(left) && typeof right === 'number' && Number.isFinite(right))
|
|
127
|
+
return true;
|
|
128
|
+
return typeof left === typeof right && (left === null) === (right === null);
|
|
129
|
+
}
|
|
130
|
+
function isNullish(cell) {
|
|
131
|
+
return !cell.present || cell.value === null || cell.value === undefined;
|
|
132
|
+
}
|
|
133
|
+
function assertJsonSerializable(value, detail) {
|
|
134
|
+
try {
|
|
135
|
+
if (JSON.stringify(value) === undefined)
|
|
136
|
+
fail('query_spec_invalid', detail);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
fail('query_spec_invalid', detail);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function assertUniqueNames(values, label) {
|
|
143
|
+
if (new Set(values).size !== values.length)
|
|
144
|
+
fail('query_spec_invalid', `${label} must contain unique names`);
|
|
145
|
+
}
|
|
146
|
+
function validateSpec(input) {
|
|
147
|
+
if (!isRecord(input))
|
|
148
|
+
fail('query_spec_invalid', 'query must be an object');
|
|
149
|
+
const allowed = new Set(['dataset_id', 'select', 'filters', 'group_by', 'aggregates', 'order_by', 'limit', 'error_policy']);
|
|
150
|
+
if (Object.keys(input).some((key) => !allowed.has(key)))
|
|
151
|
+
fail('query_spec_invalid', 'query contains unsupported fields');
|
|
152
|
+
if (!validName(input.dataset_id))
|
|
153
|
+
fail('query_spec_invalid', 'dataset_id must be a non-empty string of at most 128 characters');
|
|
154
|
+
const selectInput = input.select;
|
|
155
|
+
if (selectInput !== undefined && (!Array.isArray(selectInput) || selectInput.length < 1 || selectInput.length > MAX_QUERY_SELECT || selectInput.some((item) => !validName(item)))) {
|
|
156
|
+
fail('query_spec_invalid', `select must contain 1-${MAX_QUERY_SELECT} column names when provided`);
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(selectInput))
|
|
159
|
+
assertUniqueNames(selectInput, 'select');
|
|
160
|
+
const filters = input.filters === undefined ? [] : input.filters;
|
|
161
|
+
if (!Array.isArray(filters) || filters.length > MAX_QUERY_FILTERS)
|
|
162
|
+
fail('query_spec_invalid', `filters must contain at most ${MAX_QUERY_FILTERS} conditions`);
|
|
163
|
+
for (const filter of filters) {
|
|
164
|
+
if (!isRecord(filter) || Object.keys(filter).some((key) => !['column', 'operator', 'value'].includes(key)) || !validName(filter.column))
|
|
165
|
+
fail('query_spec_invalid', 'each filter requires a valid column');
|
|
166
|
+
const operators = ['=', '!=', '>', '>=', '<', '<=', 'in', 'is_null', 'not_null'];
|
|
167
|
+
if (typeof filter.operator !== 'string' || !operators.includes(filter.operator))
|
|
168
|
+
fail('query_spec_invalid', 'filter operator is invalid');
|
|
169
|
+
const operator = filter.operator;
|
|
170
|
+
if (operator === 'is_null' || operator === 'not_null') {
|
|
171
|
+
if (own(filter, 'value'))
|
|
172
|
+
fail('query_spec_invalid', `${operator} does not accept value`);
|
|
173
|
+
}
|
|
174
|
+
else if (!own(filter, 'value')) {
|
|
175
|
+
fail('query_spec_invalid', `${operator} requires value`);
|
|
176
|
+
}
|
|
177
|
+
if (operator === 'in' && (!Array.isArray(filter.value) || filter.value.length === 0 || filter.value.length > 64))
|
|
178
|
+
fail('query_spec_invalid', 'in requires 1-64 values');
|
|
179
|
+
if (own(filter, 'value')) {
|
|
180
|
+
const values = operator === 'in' ? filter.value : [filter.value];
|
|
181
|
+
if (values.some((value) => value === null || value === undefined))
|
|
182
|
+
fail('query_type_conflict', `${operator} cannot compare null; use is_null or not_null`);
|
|
183
|
+
assertJsonSerializable(filter.value, 'filter value must be JSON-serializable');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const groupBy = input.group_by === undefined ? [] : input.group_by;
|
|
187
|
+
if (!Array.isArray(groupBy) || groupBy.length > MAX_QUERY_GROUP_BY || groupBy.some((item) => !validName(item)))
|
|
188
|
+
fail('query_spec_invalid', `group_by must contain at most ${MAX_QUERY_GROUP_BY} column names`);
|
|
189
|
+
assertUniqueNames(groupBy, 'group_by');
|
|
190
|
+
const aggregates = input.aggregates === undefined ? [] : input.aggregates;
|
|
191
|
+
if (!Array.isArray(aggregates) || aggregates.length > MAX_QUERY_AGGREGATES)
|
|
192
|
+
fail('query_spec_invalid', `aggregates must contain at most ${MAX_QUERY_AGGREGATES} items`);
|
|
193
|
+
const normalizedAggregates = [];
|
|
194
|
+
const aggregateAliases = new Set();
|
|
195
|
+
for (const aggregate of aggregates) {
|
|
196
|
+
if (!isRecord(aggregate) || Object.keys(aggregate).some((key) => !['function', 'column', 'as'].includes(key)) || !validName(aggregate.as))
|
|
197
|
+
fail('query_spec_invalid', 'each aggregate requires a valid as alias');
|
|
198
|
+
const functions = ['count', 'min', 'max', 'avg', 'sum'];
|
|
199
|
+
if (typeof aggregate.function !== 'string' || !functions.includes(aggregate.function))
|
|
200
|
+
fail('query_spec_invalid', 'aggregate function is invalid');
|
|
201
|
+
const fn = aggregate.function;
|
|
202
|
+
if (fn === 'count') {
|
|
203
|
+
if (own(aggregate, 'column') && !validName(aggregate.column))
|
|
204
|
+
fail('query_spec_invalid', 'count column must be a valid column name');
|
|
205
|
+
}
|
|
206
|
+
else if (!validName(aggregate.column)) {
|
|
207
|
+
fail('query_spec_invalid', `${fn} requires a column`);
|
|
208
|
+
}
|
|
209
|
+
if (aggregateAliases.has(aggregate.as) || groupBy.includes(aggregate.as))
|
|
210
|
+
fail('query_spec_invalid', `aggregate alias ${aggregate.as} collides with another result column`);
|
|
211
|
+
aggregateAliases.add(aggregate.as);
|
|
212
|
+
normalizedAggregates.push({ function: fn, ...(aggregate.column === undefined ? {} : { column: aggregate.column }), as: aggregate.as });
|
|
213
|
+
}
|
|
214
|
+
if (groupBy.length === 0 && normalizedAggregates.length === 0)
|
|
215
|
+
fail('query_spec_invalid', 'query must include group_by or aggregates; raw row selection is not allowed (求首末/最新值请用 profile 的 time_facts,query 取不到原始行)');
|
|
216
|
+
const resultColumns = [...groupBy, ...normalizedAggregates.map((aggregate) => aggregate.as)];
|
|
217
|
+
const select = selectInput === undefined ? resultColumns : selectInput;
|
|
218
|
+
for (const column of select)
|
|
219
|
+
if (!resultColumns.includes(column))
|
|
220
|
+
fail('query_spec_invalid', `select column ${column} must be a group_by column or aggregate alias`);
|
|
221
|
+
const orderBy = input.order_by === undefined ? [] : input.order_by;
|
|
222
|
+
if (!Array.isArray(orderBy) || orderBy.length > MAX_QUERY_ORDER_BY)
|
|
223
|
+
fail('query_spec_invalid', `order_by must contain at most ${MAX_QUERY_ORDER_BY} items`);
|
|
224
|
+
const normalizedOrderBy = [];
|
|
225
|
+
for (const order of orderBy) {
|
|
226
|
+
if (!isRecord(order) || Object.keys(order).some((key) => !['column', 'direction'].includes(key)) || !validName(order.column) || !['asc', 'desc'].includes(String(order.direction)))
|
|
227
|
+
fail('query_spec_invalid', 'order_by item is invalid');
|
|
228
|
+
if (!resultColumns.includes(order.column))
|
|
229
|
+
fail('query_spec_invalid', `order_by column ${order.column} is not a result column`);
|
|
230
|
+
normalizedOrderBy.push({ column: order.column, direction: order.direction });
|
|
231
|
+
}
|
|
232
|
+
assertUniqueNames(normalizedOrderBy.map((item) => item.column), 'order_by');
|
|
233
|
+
const limit = input.limit === undefined ? DEFAULT_QUERY_LIMIT : input.limit;
|
|
234
|
+
if (typeof limit !== 'number' || !Number.isSafeInteger(limit) || limit < 1 || limit > MAX_QUERY_LIMIT)
|
|
235
|
+
fail('query_spec_invalid', `limit must be between 1 and ${MAX_QUERY_LIMIT}`);
|
|
236
|
+
const errorPolicy = input.error_policy === undefined ? 'skip_with_warning' : input.error_policy;
|
|
237
|
+
if (errorPolicy !== 'strict' && errorPolicy !== 'skip_with_warning')
|
|
238
|
+
fail('query_spec_invalid', 'error_policy must be strict or skip_with_warning');
|
|
239
|
+
const query = {
|
|
240
|
+
dataset_id: input.dataset_id,
|
|
241
|
+
select,
|
|
242
|
+
filters: filters,
|
|
243
|
+
group_by: groupBy,
|
|
244
|
+
aggregates: normalizedAggregates,
|
|
245
|
+
order_by: normalizedOrderBy,
|
|
246
|
+
limit,
|
|
247
|
+
error_policy: errorPolicy,
|
|
248
|
+
};
|
|
249
|
+
assertJsonSerializable(query, 'query must be JSON-serializable');
|
|
250
|
+
if (byteLength(JSON.stringify(query)) > MAX_QUERY_SPEC_BYTES)
|
|
251
|
+
fail('query_spec_invalid', `query exceeds ${MAX_QUERY_SPEC_BYTES} bytes`);
|
|
252
|
+
return query;
|
|
253
|
+
}
|
|
254
|
+
function availableColumns(rows, contracts) {
|
|
255
|
+
const columns = new Set(Object.keys(contracts));
|
|
256
|
+
for (const row of rows) {
|
|
257
|
+
if (isRecord(row))
|
|
258
|
+
for (const column of Object.keys(row))
|
|
259
|
+
columns.add(column);
|
|
260
|
+
else
|
|
261
|
+
columns.add('value');
|
|
262
|
+
}
|
|
263
|
+
return columns;
|
|
264
|
+
}
|
|
265
|
+
function ensureColumns(query, rows, contracts) {
|
|
266
|
+
const available = availableColumns(rows, contracts);
|
|
267
|
+
const names = new Set([
|
|
268
|
+
...(query.filters ?? []).map((filter) => filter.column),
|
|
269
|
+
...(query.group_by ?? []),
|
|
270
|
+
...(query.aggregates ?? []).flatMap((aggregate) => aggregate.column ? [aggregate.column] : []),
|
|
271
|
+
]);
|
|
272
|
+
for (const column of names)
|
|
273
|
+
if (!available.has(column))
|
|
274
|
+
fail('query_column_not_found', `column ${column} is not present in Dataset`);
|
|
275
|
+
}
|
|
276
|
+
function normalizedComparable(value, contract, warnings, column) {
|
|
277
|
+
if (contract?.allowNumericString && typeof value === 'string' && numericString(value) !== undefined) {
|
|
278
|
+
warnings.add(`column ${column} uses numeric-compatible string values`);
|
|
279
|
+
return numericString(value);
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
function incompatibleValue(policy, warnings, detail) {
|
|
284
|
+
if (policy === 'strict')
|
|
285
|
+
fail('query_type_conflict', detail);
|
|
286
|
+
warnings.add(detail);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
function compare(left, right, operator, contract, warnings, column, policy) {
|
|
290
|
+
if (left === null || left === undefined || right === null || right === undefined)
|
|
291
|
+
return false;
|
|
292
|
+
const normalizedLeft = normalizedComparable(left, contract, warnings, column);
|
|
293
|
+
const normalizedRight = normalizedComparable(right, contract, warnings, column);
|
|
294
|
+
if (operator === '=' || operator === '!=') {
|
|
295
|
+
const comparable = (typeof normalizedLeft === 'object' || typeof normalizedRight === 'object')
|
|
296
|
+
|| ['string', 'number', 'boolean'].includes(typeof normalizedLeft) && ['string', 'number', 'boolean'].includes(typeof normalizedRight);
|
|
297
|
+
if (!comparable || (!sameType(normalizedLeft, normalizedRight) && typeof normalizedLeft !== 'object' && typeof normalizedRight !== 'object')) {
|
|
298
|
+
return incompatibleValue(policy, warnings, `filter ${column} skipped incompatible value (${observedType(left)})`);
|
|
299
|
+
}
|
|
300
|
+
const equal = typeof normalizedLeft === 'object' || typeof normalizedRight === 'object'
|
|
301
|
+
? stableValue(normalizedLeft) === stableValue(normalizedRight)
|
|
302
|
+
: sameType(normalizedLeft, normalizedRight) && normalizedLeft === normalizedRight;
|
|
303
|
+
return operator === '=' ? equal : !equal;
|
|
304
|
+
}
|
|
305
|
+
if (operator === 'in')
|
|
306
|
+
return right.some((item) => compare(left, item, '=', contract, warnings, column, policy));
|
|
307
|
+
if (typeof normalizedLeft === 'number' && typeof normalizedRight === 'number') {
|
|
308
|
+
if (operator === '>')
|
|
309
|
+
return normalizedLeft > normalizedRight;
|
|
310
|
+
if (operator === '>=')
|
|
311
|
+
return normalizedLeft >= normalizedRight;
|
|
312
|
+
if (operator === '<')
|
|
313
|
+
return normalizedLeft < normalizedRight;
|
|
314
|
+
if (operator === '<=')
|
|
315
|
+
return normalizedLeft <= normalizedRight;
|
|
316
|
+
}
|
|
317
|
+
if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string') {
|
|
318
|
+
if (operator === '>')
|
|
319
|
+
return normalizedLeft > normalizedRight;
|
|
320
|
+
if (operator === '>=')
|
|
321
|
+
return normalizedLeft >= normalizedRight;
|
|
322
|
+
if (operator === '<')
|
|
323
|
+
return normalizedLeft < normalizedRight;
|
|
324
|
+
if (operator === '<=')
|
|
325
|
+
return normalizedLeft <= normalizedRight;
|
|
326
|
+
}
|
|
327
|
+
return incompatibleValue(policy, warnings, `filter ${column} skipped incompatible value (${observedType(left)})`);
|
|
328
|
+
}
|
|
329
|
+
function matchesFilters(row, filters, contracts, warnings, policy) {
|
|
330
|
+
for (const filter of filters) {
|
|
331
|
+
const cell = rowCell(row, filter.column);
|
|
332
|
+
if (filter.operator === 'is_null') {
|
|
333
|
+
if (!isNullish(cell))
|
|
334
|
+
return false;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (filter.operator === 'not_null') {
|
|
338
|
+
if (isNullish(cell))
|
|
339
|
+
return false;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (isNullish(cell))
|
|
343
|
+
return false;
|
|
344
|
+
if (!compare(cell.value, filter.value, filter.operator, contracts[filter.column], warnings, filter.column, policy))
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
function scalarGroupValue(value, column) {
|
|
350
|
+
if (value === null || value === undefined || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
351
|
+
return value ?? null;
|
|
352
|
+
fail('query_type_conflict', `group_by column ${column} must contain scalar values`);
|
|
353
|
+
}
|
|
354
|
+
function aggregateValue(aggregate, state, contracts, warnings, policy) {
|
|
355
|
+
const values = aggregate.column
|
|
356
|
+
? state.rows.map((row) => rowCell(row, aggregate.column)).filter((cell) => !isNullish(cell))
|
|
357
|
+
: [];
|
|
358
|
+
if (aggregate.function === 'count')
|
|
359
|
+
return aggregate.column ? values.length : state.rows.length;
|
|
360
|
+
const column = aggregate.column;
|
|
361
|
+
const numbers = [];
|
|
362
|
+
for (const cell of values) {
|
|
363
|
+
const number = numericValue(cell.value, contracts[column]);
|
|
364
|
+
if (number === undefined) {
|
|
365
|
+
if (policy === 'strict')
|
|
366
|
+
fail('query_type_conflict', `${aggregate.function} on ${column} encountered incompatible ${observedType(cell.value)} value`);
|
|
367
|
+
warnings.add(`${aggregate.function} on ${column} skipped incompatible values`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (contracts[column]?.allowNumericString && typeof cell.value === 'string')
|
|
371
|
+
warnings.add(`column ${column} uses numeric-compatible string values`);
|
|
372
|
+
numbers.push(number);
|
|
373
|
+
}
|
|
374
|
+
if (numbers.length === 0)
|
|
375
|
+
return null;
|
|
376
|
+
if (aggregate.function === 'min')
|
|
377
|
+
return numbers.reduce((minimum, number) => Math.min(minimum, number), numbers[0]);
|
|
378
|
+
if (aggregate.function === 'max')
|
|
379
|
+
return numbers.reduce((maximum, number) => Math.max(maximum, number), numbers[0]);
|
|
380
|
+
const sum = numbers.reduce((total, number) => total + number, 0);
|
|
381
|
+
if (!Number.isFinite(sum))
|
|
382
|
+
fail('query_type_conflict', `${aggregate.function} on ${column} produced a non-finite result`);
|
|
383
|
+
return aggregate.function === 'avg' ? sum / numbers.length : sum;
|
|
384
|
+
}
|
|
385
|
+
function compareOrder(left, right) {
|
|
386
|
+
if (left === right)
|
|
387
|
+
return 0;
|
|
388
|
+
if (left === null || left === undefined)
|
|
389
|
+
return 1;
|
|
390
|
+
if (right === null || right === undefined)
|
|
391
|
+
return -1;
|
|
392
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
393
|
+
return left - right;
|
|
394
|
+
const leftText = typeof left === 'string' ? left : stableValue(left);
|
|
395
|
+
const rightText = typeof right === 'string' ? right : stableValue(right);
|
|
396
|
+
return leftText < rightText ? -1 : leftText > rightText ? 1 : 0;
|
|
397
|
+
}
|
|
398
|
+
export class JsonRowsQueryEngine {
|
|
399
|
+
execute(rows, schema, input) {
|
|
400
|
+
if (!Array.isArray(rows))
|
|
401
|
+
fail('query_rows_invalid', 'Dataset rows must be an array');
|
|
402
|
+
const query = validateSpec(input);
|
|
403
|
+
const contracts = extractContracts(schema);
|
|
404
|
+
ensureColumns(query, rows, contracts);
|
|
405
|
+
const warnings = new Set();
|
|
406
|
+
const policy = query.error_policy;
|
|
407
|
+
const filtered = rows.filter((row) => matchesFilters(row, query.filters ?? [], contracts, warnings, policy));
|
|
408
|
+
const groupBy = query.group_by ?? [];
|
|
409
|
+
const aggregateList = query.aggregates ?? [];
|
|
410
|
+
const groups = new Map();
|
|
411
|
+
if (groupBy.length === 0) {
|
|
412
|
+
groups.set('global', { values: {}, rows: filtered });
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
for (const row of filtered) {
|
|
416
|
+
const values = {};
|
|
417
|
+
for (const column of groupBy)
|
|
418
|
+
values[column] = scalarGroupValue(rowCell(row, column).value, column);
|
|
419
|
+
const key = stableValue(values);
|
|
420
|
+
let group = groups.get(key);
|
|
421
|
+
if (!group) {
|
|
422
|
+
if (groups.size >= MAX_QUERY_GROUPS)
|
|
423
|
+
fail('query_group_limit_exceeded', `query produced more than ${MAX_QUERY_GROUPS} groups`);
|
|
424
|
+
group = { values, rows: [] };
|
|
425
|
+
groups.set(key, group);
|
|
426
|
+
}
|
|
427
|
+
group.rows.push(row);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const resultRows = [...groups.values()].map((group) => {
|
|
431
|
+
const row = {};
|
|
432
|
+
for (const column of groupBy)
|
|
433
|
+
row[column] = group.values[column];
|
|
434
|
+
for (const aggregate of aggregateList)
|
|
435
|
+
row[aggregate.as] = aggregateValue(aggregate, group, contracts, warnings, policy);
|
|
436
|
+
return row;
|
|
437
|
+
});
|
|
438
|
+
const orderBy = query.order_by ?? [];
|
|
439
|
+
resultRows.sort((left, right) => {
|
|
440
|
+
for (const order of orderBy) {
|
|
441
|
+
const compared = compareOrder(left[order.column], right[order.column]);
|
|
442
|
+
if (compared !== 0)
|
|
443
|
+
return order.direction === 'asc' ? compared : -compared;
|
|
444
|
+
}
|
|
445
|
+
return 0;
|
|
446
|
+
});
|
|
447
|
+
const limitedRows = resultRows.slice(0, query.limit);
|
|
448
|
+
const selectedColumns = query.select ?? [];
|
|
449
|
+
const outputRows = limitedRows.map((row) => {
|
|
450
|
+
const projected = {};
|
|
451
|
+
for (const column of selectedColumns)
|
|
452
|
+
projected[column] = row[column];
|
|
453
|
+
return projected;
|
|
454
|
+
});
|
|
455
|
+
const result = {
|
|
456
|
+
dataset_id: query.dataset_id,
|
|
457
|
+
columns: [...selectedColumns],
|
|
458
|
+
rows: outputRows,
|
|
459
|
+
matched_row_count: filtered.length,
|
|
460
|
+
group_count: resultRows.length,
|
|
461
|
+
returned_count: outputRows.length,
|
|
462
|
+
limit: query.limit,
|
|
463
|
+
warnings: [...warnings],
|
|
464
|
+
};
|
|
465
|
+
const encoded = JSON.stringify(result);
|
|
466
|
+
if (byteLength(encoded) > MAX_QUERY_OUTPUT_BYTES)
|
|
467
|
+
fail('query_result_too_large', `query result exceeds ${MAX_QUERY_OUTPUT_BYTES} bytes`);
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
export function executeJsonRowsQuery(rows, schema, query) {
|
|
472
|
+
return new JsonRowsQueryEngine().execute(rows, schema, query);
|
|
473
|
+
}
|