@kaelio/ktx 0.8.0 → 0.10.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/assets/python/{kaelio_ktx-0.8.0-py3-none-any.whl → kaelio_ktx-0.10.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/clack.d.ts +6 -0
- package/dist/clack.js +17 -2
- package/dist/cli-program.d.ts +3 -0
- package/dist/cli-program.js +42 -2
- package/dist/cli-runtime.d.ts +3 -0
- package/dist/cli-runtime.js +94 -3
- package/dist/commands/setup-commands.js +3 -4
- package/dist/connection-recovery.d.ts +34 -0
- package/dist/connection-recovery.js +82 -0
- package/dist/connection.js +26 -2
- package/dist/connectors/bigquery/connector.d.ts +2 -5
- package/dist/connectors/bigquery/connector.js +2 -2
- package/dist/connectors/clickhouse/connector.d.ts +2 -5
- package/dist/connectors/clickhouse/connector.js +2 -2
- package/dist/connectors/mysql/connector.d.ts +7 -6
- package/dist/connectors/mysql/connector.js +25 -5
- package/dist/connectors/mysql/dialect.d.ts +1 -1
- package/dist/connectors/mysql/dialect.js +12 -2
- package/dist/connectors/postgres/connector.d.ts +2 -5
- package/dist/connectors/postgres/connector.js +2 -2
- package/dist/connectors/snowflake/connector.d.ts +2 -5
- package/dist/connectors/snowflake/connector.js +2 -2
- package/dist/connectors/sqlite/connector.d.ts +2 -5
- package/dist/connectors/sqlite/connector.js +2 -2
- package/dist/connectors/sqlserver/connector.d.ts +2 -5
- package/dist/connectors/sqlserver/connector.js +2 -2
- package/dist/context/connections/drivers.d.ts +0 -1
- package/dist/context/connections/drivers.js +0 -7
- package/dist/context/connections/query-executor.d.ts +2 -1
- package/dist/context/core/abort.d.ts +9 -0
- package/dist/context/core/abort.js +36 -0
- package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
- package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
- package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
- package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
- package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +30 -0
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +194 -0
- package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
- package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
- package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
- package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
- package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
- package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
- package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
- package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
- package/dist/context/ingest/context-candidates/curator-pagination.service.d.ts +1 -5
- package/dist/context/ingest/context-candidates/curator-pagination.service.js +1 -3
- package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
- package/dist/context/ingest/final-gate-repair.d.ts +1 -0
- package/dist/context/ingest/final-gate-repair.js +1 -0
- package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
- package/dist/context/ingest/ingest-bundle.runner.js +127 -53
- package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +1 -0
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +1 -0
- package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
- package/dist/context/ingest/local-adapters.js +21 -4
- package/dist/context/ingest/local-bundle-runtime.js +13 -5
- package/dist/context/ingest/local-ingest.d.ts +1 -0
- package/dist/context/ingest/local-ingest.js +13 -3
- package/dist/context/ingest/memory-flow/events.js +1 -1
- package/dist/context/ingest/memory-flow/schema.js +8 -3
- package/dist/context/ingest/memory-flow/types.d.ts +7 -3
- package/dist/context/ingest/ports.d.ts +3 -5
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +1 -4
- package/dist/context/ingest/stages/stage-3-work-units.js +5 -1
- package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +1 -4
- package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
- package/dist/context/ingest/types.d.ts +1 -0
- package/dist/context/llm/ai-sdk-runtime.d.ts +3 -0
- package/dist/context/llm/ai-sdk-runtime.js +152 -16
- package/dist/context/llm/claude-code-runtime.d.ts +6 -4
- package/dist/context/llm/claude-code-runtime.js +127 -48
- package/dist/context/llm/codex-exec-events.d.ts +20 -0
- package/dist/context/llm/codex-exec-events.js +155 -0
- package/dist/context/llm/codex-isolation.d.ts +3 -0
- package/dist/context/llm/codex-isolation.js +5 -0
- package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
- package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
- package/dist/context/llm/codex-models.d.ts +2 -0
- package/dist/context/llm/codex-models.js +17 -0
- package/dist/context/llm/codex-runtime-config.d.ts +16 -0
- package/dist/context/llm/codex-runtime-config.js +19 -0
- package/dist/context/llm/codex-runtime.d.ts +37 -0
- package/dist/context/llm/codex-runtime.js +347 -0
- package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
- package/dist/context/llm/codex-sdk-runner.js +63 -0
- package/dist/context/llm/local-config.d.ts +16 -4
- package/dist/context/llm/local-config.js +18 -2
- package/dist/context/llm/rate-limit-governor.d.ts +103 -0
- package/dist/context/llm/rate-limit-governor.js +285 -0
- package/dist/context/llm/runtime-port.d.ts +3 -6
- package/dist/context/mcp/context-tools.js +43 -13
- package/dist/context/project/config.d.ts +14 -0
- package/dist/context/project/config.js +37 -2
- package/dist/context/scan/types.d.ts +15 -2
- package/dist/context/scan/types.js +12 -0
- package/dist/context/sl/description-normalization.js +4 -14
- package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
- package/dist/context/sql-analysis/ports.d.ts +12 -2
- package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
- package/dist/context-build-view.d.ts +13 -0
- package/dist/context-build-view.js +63 -32
- package/dist/demo-metrics.d.ts +0 -2
- package/dist/demo-metrics.js +1 -11
- package/dist/ingest.d.ts +1 -0
- package/dist/ingest.js +32 -3
- package/dist/io/buffered-command-io.d.ts +11 -0
- package/dist/io/buffered-command-io.js +28 -0
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- package/dist/llm/types.d.ts +1 -1
- package/dist/local-adapters.d.ts +10 -2
- package/dist/local-adapters.js +19 -3
- package/dist/memory-flow-hud.js +8 -16
- package/dist/next-steps.js +1 -2
- package/dist/progress-port-adapter.d.ts +6 -0
- package/dist/progress-port-adapter.js +18 -0
- package/dist/public-ingest.d.ts +20 -1
- package/dist/public-ingest.js +228 -42
- package/dist/reveal-password-prompt.d.ts +24 -0
- package/dist/reveal-password-prompt.js +78 -0
- package/dist/scan.js +21 -3
- package/dist/setup-context.d.ts +2 -0
- package/dist/setup-context.js +133 -27
- package/dist/setup-databases.d.ts +18 -1
- package/dist/setup-databases.js +378 -249
- package/dist/setup-demo-tour.js +1 -0
- package/dist/setup-embeddings.js +1 -1
- package/dist/setup-models.d.ts +11 -15
- package/dist/setup-models.js +140 -276
- package/dist/setup-prompts.js +3 -2
- package/dist/setup-ready-menu.d.ts +16 -2
- package/dist/setup-ready-menu.js +37 -5
- package/dist/setup-sources.js +115 -35
- package/dist/setup.d.ts +1 -1
- package/dist/setup.js +23 -11
- package/dist/sl.d.ts +2 -2
- package/dist/sl.js +20 -4
- package/dist/sql.js +18 -2
- package/dist/star-prompt/cache.d.ts +16 -0
- package/dist/star-prompt/cache.js +45 -0
- package/dist/star-prompt/star-count.d.ts +7 -0
- package/dist/star-prompt/star-count.js +66 -0
- package/dist/star-prompt/star-line.d.ts +12 -0
- package/dist/star-prompt/star-line.js +26 -0
- package/dist/status-project.d.ts +11 -0
- package/dist/status-project.js +50 -1
- package/dist/telemetry/command-hook.d.ts +1 -0
- package/dist/telemetry/command-hook.js +3 -1
- package/dist/telemetry/emitter.d.ts +10 -0
- package/dist/telemetry/emitter.js +31 -0
- package/dist/telemetry/events.d.ts +35 -6
- package/dist/telemetry/events.js +25 -2
- package/dist/telemetry/exception.d.ts +18 -0
- package/dist/telemetry/exception.js +162 -0
- package/dist/telemetry/identity.d.ts +0 -1
- package/dist/telemetry/identity.js +6 -6
- package/dist/telemetry/index.d.ts +15 -2
- package/dist/telemetry/index.js +15 -3
- package/dist/telemetry/redaction-secrets.d.ts +11 -0
- package/dist/telemetry/redaction-secrets.js +92 -0
- package/dist/telemetry/scrubber.d.ts +10 -0
- package/dist/telemetry/scrubber.js +20 -0
- package/dist/update-check/cache.d.ts +21 -0
- package/dist/update-check/cache.js +38 -0
- package/dist/update-check/channel.d.ts +15 -0
- package/dist/update-check/channel.js +30 -0
- package/dist/update-check/registry.d.ts +1 -0
- package/dist/update-check/registry.js +45 -0
- package/dist/update-check/update-check.d.ts +43 -0
- package/dist/update-check/update-check.js +116 -0
- package/package.json +12 -4
- package/dist/context/connections/local-query-executor.d.ts +0 -6
- package/dist/context/connections/local-query-executor.js +0 -39
- package/dist/context/connections/postgres-query-executor.d.ts +0 -25
- package/dist/context/connections/postgres-query-executor.js +0 -53
- package/dist/context/connections/sqlite-query-executor.d.ts +0 -4
- package/dist/context/connections/sqlite-query-executor.js +0 -74
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tableRefKey } from '../../../scan/table-ref.js';
|
|
3
|
+
import { bucketDistinctUsers, bucketExecutions, bucketRecency } from './buckets.js';
|
|
4
|
+
import { compileHistoricSqlRedactionPatterns, redactHistoricSqlText, } from './redaction.js';
|
|
5
|
+
import { includedQueryHistoryTableRefs } from './scope-membership.js';
|
|
6
|
+
import { aggregatedTemplateSchema, historicSqlUnifiedPullConfigSchema, } from './types.js';
|
|
7
|
+
const queryHistoryFilterAdjudicationSchema = z.object({
|
|
8
|
+
roles: z.array(z.object({
|
|
9
|
+
role: z.string().min(1),
|
|
10
|
+
exclude: z.boolean(),
|
|
11
|
+
reason: z.string().min(1),
|
|
12
|
+
}).strict()),
|
|
13
|
+
}).strict();
|
|
14
|
+
function emptyProposal(skipped, warnings = []) {
|
|
15
|
+
return { excludedRoles: [], consideredRoleCount: 0, skipped, warnings, parseFailedTemplateIds: [] };
|
|
16
|
+
}
|
|
17
|
+
function displayTableRef(ref) {
|
|
18
|
+
return [ref.catalog, ref.db, ref.name].filter((part) => !!part && part.length > 0).join('.');
|
|
19
|
+
}
|
|
20
|
+
function redactTemplateSqlForPicker(template, redactors) {
|
|
21
|
+
if (redactors.length === 0) {
|
|
22
|
+
return template;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...template,
|
|
26
|
+
canonicalSql: redactHistoricSqlText(template.canonicalSql, redactors),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** @internal */
|
|
30
|
+
export function regexEscapeForExactRolePattern(role) {
|
|
31
|
+
return `^${role.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')}$`;
|
|
32
|
+
}
|
|
33
|
+
function recordRole(acc, template, tables, executions) {
|
|
34
|
+
acc.executions += executions;
|
|
35
|
+
acc.distinctUsers = Math.max(acc.distinctUsers, template.stats.distinctUsers);
|
|
36
|
+
acc.lastSeen = template.stats.lastSeen > acc.lastSeen ? template.stats.lastSeen : acc.lastSeen;
|
|
37
|
+
for (const table of tables) {
|
|
38
|
+
acc.tables.set(tableRefKey(table), table);
|
|
39
|
+
}
|
|
40
|
+
acc.templates.push(template);
|
|
41
|
+
}
|
|
42
|
+
function roleRecords(parsedTemplates, now) {
|
|
43
|
+
const byRole = new Map();
|
|
44
|
+
for (const parsed of parsedTemplates) {
|
|
45
|
+
for (const entry of parsed.template.topUsers) {
|
|
46
|
+
if (!entry.user || entry.user.trim().length === 0 || entry.executions <= 0) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const role = entry.user.trim();
|
|
50
|
+
const acc = byRole.get(role) ??
|
|
51
|
+
{
|
|
52
|
+
role,
|
|
53
|
+
executions: 0,
|
|
54
|
+
distinctUsers: 0,
|
|
55
|
+
lastSeen: '1970-01-01T00:00:00.000Z',
|
|
56
|
+
tables: new Map(),
|
|
57
|
+
templates: [],
|
|
58
|
+
};
|
|
59
|
+
recordRole(acc, parsed.template, parsed.includedTables, entry.executions);
|
|
60
|
+
byRole.set(role, acc);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...byRole.values()]
|
|
64
|
+
.sort((left, right) => right.executions - left.executions || left.role.localeCompare(right.role))
|
|
65
|
+
.map((acc) => ({
|
|
66
|
+
role: acc.role,
|
|
67
|
+
inScopeTables: [...acc.tables.entries()]
|
|
68
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
69
|
+
.slice(0, 25)
|
|
70
|
+
.map(([, ref]) => displayTableRef(ref)),
|
|
71
|
+
executionsBucket: bucketExecutions(acc.executions),
|
|
72
|
+
distinctUsersBucket: bucketDistinctUsers(acc.distinctUsers),
|
|
73
|
+
recencyBucket: bucketRecency(acc.lastSeen, now),
|
|
74
|
+
representativeTemplates: [...acc.templates]
|
|
75
|
+
.sort((left, right) => right.stats.executions - left.stats.executions || left.templateId.localeCompare(right.templateId))
|
|
76
|
+
.slice(0, 3)
|
|
77
|
+
.map((template) => ({
|
|
78
|
+
id: template.templateId,
|
|
79
|
+
canonicalSql: template.canonicalSql,
|
|
80
|
+
dialect: template.dialect,
|
|
81
|
+
})),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function adjudicationSystemPrompt() {
|
|
85
|
+
return [
|
|
86
|
+
'You are helping ktx decide whether observed query-history roles are operational service accounts.',
|
|
87
|
+
'Default every role to keep. Mark exclude true only when the aggregate evidence clearly shows loader, ELT, reverse-ETL, export, refresh, or maintenance traffic rather than analyst or BI-dashboard usage.',
|
|
88
|
+
'Use only the observed role records. Do not rely on a hardcoded denylist. Return structured output only.',
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
91
|
+
export async function proposeQueryHistoryServiceAccountFilters(input) {
|
|
92
|
+
if (!input.llmRuntime) {
|
|
93
|
+
return emptyProposal({ reason: 'no-llm' });
|
|
94
|
+
}
|
|
95
|
+
const config = historicSqlUnifiedPullConfigSchema.parse(input.pullConfig);
|
|
96
|
+
const redactors = compileHistoricSqlRedactionPatterns(config.redactionPatterns);
|
|
97
|
+
const now = input.now ?? new Date();
|
|
98
|
+
const windowDays = 'windowDays' in config ? config.windowDays : 90;
|
|
99
|
+
const windowStart = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
|
|
100
|
+
const warnings = [];
|
|
101
|
+
const parseFailedTemplateIds = [];
|
|
102
|
+
const snapshot = [];
|
|
103
|
+
try {
|
|
104
|
+
for await (const row of input.reader.fetchAggregated(input.queryClient, { start: windowStart, end: now }, config)) {
|
|
105
|
+
snapshot.push(aggregatedTemplateSchema.parse(row));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return emptyProposal(null, [
|
|
110
|
+
`query_history_filter_picker_read_failed:${error instanceof Error ? error.message : String(error)}`,
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
if (snapshot.length === 0) {
|
|
114
|
+
return emptyProposal({ reason: 'no-in-scope-history' });
|
|
115
|
+
}
|
|
116
|
+
const analysisItems = snapshot.map((template) => ({ id: template.templateId, sql: template.canonicalSql }));
|
|
117
|
+
const analysisOptions = config.modeledTableCatalog.length > 0 ? { catalog: { tables: config.modeledTableCatalog } } : undefined;
|
|
118
|
+
let analysis;
|
|
119
|
+
try {
|
|
120
|
+
analysis = await input.sqlAnalysis.analyzeBatch(analysisItems, input.dialect, analysisOptions);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return emptyProposal({ reason: 'no-daemon' }, [
|
|
124
|
+
`query_history_filter_picker_analysis_failed:${error instanceof Error ? error.message : String(error)}`,
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
const parsedTemplates = [];
|
|
128
|
+
for (const template of snapshot) {
|
|
129
|
+
const parsed = analysis.get(template.templateId);
|
|
130
|
+
if (!parsed || parsed.error) {
|
|
131
|
+
parseFailedTemplateIds.push(template.templateId);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const tablesTouched = [...new Map(parsed.tablesTouched.map((ref) => [tableRefKey(ref), ref])).values()]
|
|
135
|
+
.filter((ref) => ref.name.length > 0)
|
|
136
|
+
.sort((left, right) => tableRefKey(left).localeCompare(tableRefKey(right)));
|
|
137
|
+
const includedTables = includedQueryHistoryTableRefs(tablesTouched, config);
|
|
138
|
+
if (includedTables.length === 0) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
parsedTemplates.push({
|
|
142
|
+
template: redactTemplateSqlForPicker(template, redactors),
|
|
143
|
+
tablesTouched,
|
|
144
|
+
includedTables,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const records = roleRecords(parsedTemplates, now);
|
|
148
|
+
if (records.length <= 1) {
|
|
149
|
+
return {
|
|
150
|
+
excludedRoles: [],
|
|
151
|
+
consideredRoleCount: records.length,
|
|
152
|
+
skipped: { reason: 'no-in-scope-history' },
|
|
153
|
+
warnings,
|
|
154
|
+
parseFailedTemplateIds,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
let generated;
|
|
158
|
+
try {
|
|
159
|
+
generated = await input.llmRuntime.generateObject({
|
|
160
|
+
role: 'candidateExtraction',
|
|
161
|
+
system: adjudicationSystemPrompt(),
|
|
162
|
+
prompt: JSON.stringify({ connectionId: input.connectionId, dialect: input.dialect, roles: records }),
|
|
163
|
+
schema: queryHistoryFilterAdjudicationSchema,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
excludedRoles: [],
|
|
169
|
+
consideredRoleCount: records.length,
|
|
170
|
+
skipped: { reason: 'no-llm' },
|
|
171
|
+
warnings: [
|
|
172
|
+
...warnings,
|
|
173
|
+
`query_history_filter_picker_llm_failed:${error instanceof Error ? error.message : String(error)}`,
|
|
174
|
+
],
|
|
175
|
+
parseFailedTemplateIds,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const knownRoles = new Set(records.map((record) => record.role));
|
|
179
|
+
const excludedRoles = generated.roles
|
|
180
|
+
.filter((role) => role.exclude && knownRoles.has(role.role))
|
|
181
|
+
.sort((left, right) => left.role.localeCompare(right.role))
|
|
182
|
+
.map((role) => ({
|
|
183
|
+
role: role.role,
|
|
184
|
+
reason: role.reason,
|
|
185
|
+
pattern: regexEscapeForExactRolePattern(role.role),
|
|
186
|
+
}));
|
|
187
|
+
return {
|
|
188
|
+
excludedRoles,
|
|
189
|
+
consideredRoleCount: records.length,
|
|
190
|
+
skipped: input.userServiceAccountsPresent ? { reason: 'user-block-present' } : null,
|
|
191
|
+
warnings,
|
|
192
|
+
parseFailedTemplateIds,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type KtxTableRefKey } from '../../../scan/table-ref.js';
|
|
2
|
+
import type { KtxTableRef } from '../../../scan/types.js';
|
|
3
|
+
export interface QueryHistoryScopeFloorInput {
|
|
4
|
+
projectDir: string;
|
|
5
|
+
connectionId: string;
|
|
6
|
+
driver: string;
|
|
7
|
+
connection: Record<string, unknown>;
|
|
8
|
+
storedQueryHistory: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface QueryHistoryScopeFloor {
|
|
11
|
+
enabledTables: KtxTableRef[];
|
|
12
|
+
enabledTableKeys: ReadonlySet<KtxTableRefKey> | null;
|
|
13
|
+
enabledSchemas: string[];
|
|
14
|
+
modeledTableCatalog: KtxTableRef[];
|
|
15
|
+
floorDisabled: boolean;
|
|
16
|
+
warnings: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveQueryHistoryScopeFloor(input: QueryHistoryScopeFloorInput): Promise<QueryHistoryScopeFloor>;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { access, readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { getDriverRegistration } from '../../../connections/drivers.js';
|
|
5
|
+
import { parseDottedTableEntry } from '../../../scan/enabled-tables.js';
|
|
6
|
+
import { tableRefKey, tableRefSet } from '../../../scan/table-ref.js';
|
|
7
|
+
import { readLiveDatabaseTableFiles } from '../live-database/stage.js';
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function stringArray(value) {
|
|
12
|
+
return Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
15
|
+
.map((item) => item.trim())
|
|
16
|
+
: [];
|
|
17
|
+
}
|
|
18
|
+
function tableRefsFromValues(values) {
|
|
19
|
+
if (!Array.isArray(values))
|
|
20
|
+
return [];
|
|
21
|
+
return values.flatMap((value) => {
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
const ref = parseDottedTableEntry(value);
|
|
24
|
+
return ref ? [ref] : [];
|
|
25
|
+
}
|
|
26
|
+
if (isRecord(value) && typeof value.name === 'string' && value.name.length > 0) {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
catalog: typeof value.catalog === 'string' ? value.catalog : null,
|
|
30
|
+
db: typeof value.db === 'string' ? value.db : null,
|
|
31
|
+
name: value.name,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function declaredSchemas(driver, connection) {
|
|
39
|
+
const key = getDriverRegistration(driver)?.scopeConfigKey;
|
|
40
|
+
if (!key)
|
|
41
|
+
return [];
|
|
42
|
+
return [...new Set(stringArray(connection[key]))].sort();
|
|
43
|
+
}
|
|
44
|
+
function uniqueSortedTableRefs(refs) {
|
|
45
|
+
const byKey = new Map();
|
|
46
|
+
for (const ref of refs) {
|
|
47
|
+
byKey.set(tableRefKey(ref), ref);
|
|
48
|
+
}
|
|
49
|
+
return [...byKey.entries()]
|
|
50
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
51
|
+
.map(([, ref]) => ref);
|
|
52
|
+
}
|
|
53
|
+
async function latestLiveDatabaseScanDir(projectDir, connectionId) {
|
|
54
|
+
const root = join(projectDir, 'raw-sources', connectionId, 'live-database');
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
|
|
61
|
+
return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const syncDirs = entries
|
|
65
|
+
.filter((entry) => entry.isDirectory())
|
|
66
|
+
.map((entry) => entry.name)
|
|
67
|
+
.sort()
|
|
68
|
+
.reverse();
|
|
69
|
+
for (const syncDir of syncDirs) {
|
|
70
|
+
const absolute = join(root, syncDir);
|
|
71
|
+
try {
|
|
72
|
+
await access(join(absolute, 'connection.json'));
|
|
73
|
+
return absolute;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
async function scannedTableRefs(projectDir, connectionId) {
|
|
82
|
+
const scanDir = await latestLiveDatabaseScanDir(projectDir, connectionId);
|
|
83
|
+
if (!scanDir) {
|
|
84
|
+
return { refs: [], catalogAvailable: false, warnings: [] };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const tableFiles = await readLiveDatabaseTableFiles(scanDir);
|
|
88
|
+
return {
|
|
89
|
+
refs: uniqueSortedTableRefs(tableFiles.map(({ table }) => ({ catalog: table.catalog, db: table.db, name: table.name }))),
|
|
90
|
+
catalogAvailable: true,
|
|
91
|
+
warnings: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
refs: [],
|
|
97
|
+
catalogAvailable: false,
|
|
98
|
+
warnings: [
|
|
99
|
+
`query_history_scope_floor_catalog_read_failed:live_database_scan:${error instanceof Error ? error.message : String(error)}`,
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function listYamlFiles(root) {
|
|
105
|
+
try {
|
|
106
|
+
const entries = await readdir(root, { withFileTypes: true, recursive: true });
|
|
107
|
+
return entries
|
|
108
|
+
.filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
|
|
109
|
+
.map((entry) => relative(root, join(entry.parentPath, entry.name)).replace(/\\/g, '/'))
|
|
110
|
+
.sort();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
|
|
114
|
+
return [];
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function refsFromManifest(content) {
|
|
119
|
+
const parsed = YAML.parse(content);
|
|
120
|
+
if (!isRecord(parsed) || !isRecord(parsed.tables))
|
|
121
|
+
return [];
|
|
122
|
+
return Object.values(parsed.tables).flatMap((entry) => {
|
|
123
|
+
if (!isRecord(entry) || typeof entry.table !== 'string')
|
|
124
|
+
return [];
|
|
125
|
+
const ref = parseDottedTableEntry(entry.table);
|
|
126
|
+
return ref ? [ref] : [];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function refsFromStandaloneSource(content) {
|
|
130
|
+
const parsed = YAML.parse(content);
|
|
131
|
+
if (!isRecord(parsed) || typeof parsed.table !== 'string')
|
|
132
|
+
return [];
|
|
133
|
+
const ref = parseDottedTableEntry(parsed.table);
|
|
134
|
+
return ref ? [ref] : [];
|
|
135
|
+
}
|
|
136
|
+
async function semanticTableRefs(projectDir, connectionId) {
|
|
137
|
+
const root = join(projectDir, 'semantic-layer', connectionId);
|
|
138
|
+
const files = await listYamlFiles(root);
|
|
139
|
+
const refs = [];
|
|
140
|
+
const warnings = [];
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
try {
|
|
143
|
+
const content = await readFile(join(root, file), 'utf-8');
|
|
144
|
+
refs.push(...(file.startsWith('_schema/') ? refsFromManifest(content) : refsFromStandaloneSource(content)));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
warnings.push(`query_history_scope_floor_catalog_read_failed:${file}:${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { refs: uniqueSortedTableRefs(refs), warnings };
|
|
151
|
+
}
|
|
152
|
+
export async function resolveQueryHistoryScopeFloor(input) {
|
|
153
|
+
const explicitEnabledTables = [
|
|
154
|
+
...tableRefsFromValues(input.storedQueryHistory.enabledTables),
|
|
155
|
+
...tableRefsFromValues(input.connection.enabled_tables),
|
|
156
|
+
];
|
|
157
|
+
const semanticTables = await semanticTableRefs(input.projectDir, input.connectionId);
|
|
158
|
+
const scannedTables = await scannedTableRefs(input.projectDir, input.connectionId);
|
|
159
|
+
const modeledTables = uniqueSortedTableRefs([
|
|
160
|
+
...semanticTables.refs,
|
|
161
|
+
...scannedTables.refs,
|
|
162
|
+
...explicitEnabledTables,
|
|
163
|
+
]);
|
|
164
|
+
const warnings = [...semanticTables.warnings, ...scannedTables.warnings];
|
|
165
|
+
if (explicitEnabledTables.length > 0) {
|
|
166
|
+
return {
|
|
167
|
+
enabledTables: explicitEnabledTables,
|
|
168
|
+
enabledTableKeys: tableRefSet(explicitEnabledTables),
|
|
169
|
+
enabledSchemas: [],
|
|
170
|
+
modeledTableCatalog: modeledTables,
|
|
171
|
+
floorDisabled: false,
|
|
172
|
+
warnings,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const explicitSchemas = stringArray(input.storedQueryHistory.enabledSchemas);
|
|
176
|
+
if (explicitSchemas.includes('*')) {
|
|
177
|
+
return {
|
|
178
|
+
enabledTables: [],
|
|
179
|
+
enabledTableKeys: null,
|
|
180
|
+
enabledSchemas: ['*'],
|
|
181
|
+
modeledTableCatalog: modeledTables,
|
|
182
|
+
floorDisabled: true,
|
|
183
|
+
warnings,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (explicitSchemas.length > 0) {
|
|
187
|
+
if (!scannedTables.catalogAvailable || modeledTables.length === 0) {
|
|
188
|
+
return {
|
|
189
|
+
enabledTables: [],
|
|
190
|
+
enabledTableKeys: null,
|
|
191
|
+
enabledSchemas: ['*'],
|
|
192
|
+
modeledTableCatalog: modeledTables,
|
|
193
|
+
floorDisabled: true,
|
|
194
|
+
warnings: [...warnings, 'query_history_scope_floor_disabled:catalog_unavailable'],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
enabledTables: [],
|
|
199
|
+
enabledTableKeys: null,
|
|
200
|
+
enabledSchemas: [...new Set(explicitSchemas)].sort(),
|
|
201
|
+
modeledTableCatalog: modeledTables,
|
|
202
|
+
floorDisabled: false,
|
|
203
|
+
warnings,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const schemas = new Set(declaredSchemas(input.driver, input.connection));
|
|
207
|
+
for (const ref of semanticTables.refs) {
|
|
208
|
+
if (ref.db)
|
|
209
|
+
schemas.add(ref.db);
|
|
210
|
+
}
|
|
211
|
+
if (schemas.size > 0 && (!scannedTables.catalogAvailable || modeledTables.length === 0)) {
|
|
212
|
+
return {
|
|
213
|
+
enabledTables: [],
|
|
214
|
+
enabledTableKeys: null,
|
|
215
|
+
enabledSchemas: ['*'],
|
|
216
|
+
modeledTableCatalog: modeledTables,
|
|
217
|
+
floorDisabled: true,
|
|
218
|
+
warnings: [...warnings, 'query_history_scope_floor_disabled:catalog_unavailable'],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
enabledTables: [],
|
|
223
|
+
enabledTableKeys: null,
|
|
224
|
+
enabledSchemas: [...schemas].sort(),
|
|
225
|
+
modeledTableCatalog: modeledTables,
|
|
226
|
+
floorDisabled: false,
|
|
227
|
+
warnings,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { KtxTableRef } from '../../../scan/types.js';
|
|
2
|
+
export interface QueryHistoryScopeMembershipConfig {
|
|
3
|
+
enabledTables: readonly KtxTableRef[];
|
|
4
|
+
enabledSchemas: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function isQueryHistoryScopeFloorDisabled(config: QueryHistoryScopeMembershipConfig): boolean;
|
|
7
|
+
export declare function shouldFailOpenQueryHistoryScope(config: QueryHistoryScopeMembershipConfig): boolean;
|
|
8
|
+
export declare function includedQueryHistoryTableRefs(tablesTouched: readonly KtxTableRef[], config: QueryHistoryScopeMembershipConfig): KtxTableRef[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { tableRefKey, tableRefSet } from '../../../scan/table-ref.js';
|
|
2
|
+
function schemaNameForRef(ref) {
|
|
3
|
+
return ref.db && ref.db.length > 0 ? ref.db : null;
|
|
4
|
+
}
|
|
5
|
+
function schemaNamesFromConfig(enabledSchemas) {
|
|
6
|
+
return new Set(enabledSchemas.filter((schema) => schema !== '*'));
|
|
7
|
+
}
|
|
8
|
+
export function isQueryHistoryScopeFloorDisabled(config) {
|
|
9
|
+
return config.enabledSchemas.includes('*');
|
|
10
|
+
}
|
|
11
|
+
export function shouldFailOpenQueryHistoryScope(config) {
|
|
12
|
+
return (config.enabledTables.length === 0 &&
|
|
13
|
+
!isQueryHistoryScopeFloorDisabled(config) &&
|
|
14
|
+
config.enabledSchemas.length === 0);
|
|
15
|
+
}
|
|
16
|
+
export function includedQueryHistoryTableRefs(tablesTouched, config) {
|
|
17
|
+
if (config.enabledTables.length > 0) {
|
|
18
|
+
const enabled = tableRefSet(config.enabledTables);
|
|
19
|
+
return tablesTouched.filter((ref) => enabled.has(tableRefKey(ref)));
|
|
20
|
+
}
|
|
21
|
+
if (isQueryHistoryScopeFloorDisabled(config) || shouldFailOpenQueryHistoryScope(config)) {
|
|
22
|
+
return [...tablesTouched];
|
|
23
|
+
}
|
|
24
|
+
const schemas = schemaNamesFromConfig(config.enabledSchemas);
|
|
25
|
+
return tablesTouched.filter((ref) => {
|
|
26
|
+
const schema = schemaNameForRef(ref);
|
|
27
|
+
return schema !== null && schemas.has(schema);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -148,26 +148,75 @@ export class SnowflakeHistoricSqlQueryHistoryReader {
|
|
|
148
148
|
}
|
|
149
149
|
async *fetchAggregated(client, window, config) {
|
|
150
150
|
const sql = `
|
|
151
|
+
WITH filtered_queries AS (
|
|
152
|
+
SELECT
|
|
153
|
+
query_hash,
|
|
154
|
+
query_text,
|
|
155
|
+
user_name,
|
|
156
|
+
start_time,
|
|
157
|
+
total_elapsed_time,
|
|
158
|
+
execution_status,
|
|
159
|
+
rows_produced
|
|
160
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
|
|
161
|
+
WHERE query_text IS NOT NULL
|
|
162
|
+
AND query_type IN ('SELECT', 'MERGE')
|
|
163
|
+
AND start_time >= ${timestampLiteral(window.start)}
|
|
164
|
+
AND start_time < ${timestampLiteral(window.end)}
|
|
165
|
+
),
|
|
166
|
+
template_stats AS (
|
|
167
|
+
SELECT
|
|
168
|
+
query_hash AS template_id,
|
|
169
|
+
MIN(query_text) AS canonical_sql,
|
|
170
|
+
COUNT(*) AS executions,
|
|
171
|
+
COUNT(DISTINCT user_name) AS distinct_users,
|
|
172
|
+
MIN(start_time) AS first_seen,
|
|
173
|
+
MAX(start_time) AS last_seen,
|
|
174
|
+
APPROX_PERCENTILE(total_elapsed_time, 0.50) AS p50_ms,
|
|
175
|
+
APPROX_PERCENTILE(total_elapsed_time, 0.95) AS p95_ms,
|
|
176
|
+
DIV0(COUNT_IF(execution_status != 'SUCCESS'), COUNT(*)) AS error_rate,
|
|
177
|
+
SUM(rows_produced) AS rows_produced
|
|
178
|
+
FROM filtered_queries
|
|
179
|
+
GROUP BY query_hash
|
|
180
|
+
HAVING COUNT(*) >= ${config.minExecutions}
|
|
181
|
+
),
|
|
182
|
+
template_users AS (
|
|
183
|
+
SELECT
|
|
184
|
+
query_hash AS template_id,
|
|
185
|
+
user_name AS user,
|
|
186
|
+
COUNT(*) AS executions,
|
|
187
|
+
MAX(start_time) AS last_seen
|
|
188
|
+
FROM filtered_queries
|
|
189
|
+
GROUP BY query_hash, user_name
|
|
190
|
+
)
|
|
151
191
|
SELECT
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
ARRAY_AGG(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
GROUP BY
|
|
169
|
-
|
|
170
|
-
|
|
192
|
+
stats.template_id,
|
|
193
|
+
stats.canonical_sql,
|
|
194
|
+
stats.executions,
|
|
195
|
+
stats.distinct_users,
|
|
196
|
+
stats.first_seen,
|
|
197
|
+
stats.last_seen,
|
|
198
|
+
stats.p50_ms,
|
|
199
|
+
stats.p95_ms,
|
|
200
|
+
stats.error_rate,
|
|
201
|
+
stats.rows_produced,
|
|
202
|
+
ARRAY_AGG(
|
|
203
|
+
OBJECT_CONSTRUCT('user', users.user, 'executions', users.executions)
|
|
204
|
+
) WITHIN GROUP (ORDER BY users.executions DESC, users.last_seen DESC)::string AS top_users
|
|
205
|
+
FROM template_stats AS stats
|
|
206
|
+
JOIN template_users AS users
|
|
207
|
+
ON users.template_id = stats.template_id
|
|
208
|
+
GROUP BY
|
|
209
|
+
stats.template_id,
|
|
210
|
+
stats.canonical_sql,
|
|
211
|
+
stats.executions,
|
|
212
|
+
stats.distinct_users,
|
|
213
|
+
stats.first_seen,
|
|
214
|
+
stats.last_seen,
|
|
215
|
+
stats.p50_ms,
|
|
216
|
+
stats.p95_ms,
|
|
217
|
+
stats.error_rate,
|
|
218
|
+
stats.rows_produced
|
|
219
|
+
ORDER BY stats.executions DESC`.trim();
|
|
171
220
|
const result = await queryClient(client).executeQuery(sql);
|
|
172
221
|
if (result.error) {
|
|
173
222
|
throw grantsError(result.error);
|