@malloydata/malloy 0.0.425 → 0.0.426

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.
@@ -374,7 +374,11 @@ class Malloy {
374
374
  connection = await connections.lookupConnection(connectionName);
375
375
  }
376
376
  if (sqlStruct) {
377
- const data = await connection.runSQL(sqlStruct.selectStr);
377
+ // Only the metadata: a SQL block's rowLimit/abortSignal handling is what
378
+ // it was, and changing that is not this feature's business.
379
+ const data = await connection.runSQL(sqlStruct.selectStr, {
380
+ queryMetadata: options === null || options === void 0 ? void 0 : options.queryMetadata,
381
+ });
378
382
  return new result_1.Result({
379
383
  structs: [sqlStruct],
380
384
  sql: sqlStruct.selectStr,
@@ -546,9 +546,7 @@ export declare class PreparedResultMaterializer extends FluentState<PreparedResu
546
546
  * @return A promise to the query result data.
547
547
  */
548
548
  run(options?: RunSQLOptions): Promise<Result>;
549
- runStream(options?: {
550
- rowLimit?: number;
551
- }): AsyncIterableIterator<DataRecord>;
549
+ runStream(options?: RunSQLOptions): AsyncIterableIterator<DataRecord>;
552
550
  /**
553
551
  * Materialize this loaded prepared result.
554
552
  *
@@ -586,33 +586,37 @@ class ModelMaterializer extends FluentState {
586
586
  * or loading further related objects.
587
587
  */
588
588
  loadQuery(query, options) {
589
- const { refreshSchemaCache, noThrowOnError } = options || {};
589
+ // `options` spans three interfaces. The parse/compile half is spent here,
590
+ // compiling the query text; the `CompileQueryOptions` half belongs to the
591
+ // materializer, which applies it when the query is turned into SQL — so it
592
+ // has to reach `makeQueryMaterializer` as well, or a `buildManifest` (or
593
+ // `givens`, or `defaultRowLimit`) passed here is silently ignored.
594
+ //
595
+ // Split by naming the parse/compile keys rather than the query ones, so a
596
+ // query option added later is carried rather than quietly dropped.
597
+ // `restrictedMode` and `method` are named to hold them back: this entry
598
+ // point has never honored them, and `loadRestrictedQuery` is how a caller
599
+ // asks for restricted compilation.
600
+ const { importBaseURL, testEnvironment, refreshSchemaCache, noThrowOnError, restrictedMode: _restrictedMode, method: _method, ...callQueryOptions } = options !== null && options !== void 0 ? options : {};
601
+ const compileQueryOptions = {
602
+ ...this.compileQueryOptions,
603
+ ...callQueryOptions,
604
+ };
590
605
  return this.makeQueryMaterializer(async () => {
591
- const urlReader = this.runtime.urlReader;
592
- const connections = this.runtime.connections;
593
- if (this.runtime.isTestRuntime) {
594
- if (options === undefined) {
595
- options = { testEnvironment: true };
596
- }
597
- else {
598
- options = { ...options, testEnvironment: true };
599
- }
600
- }
601
606
  const compilable = query instanceof URL ? { url: query } : { source: query };
602
- const model = await this.getModel();
603
607
  const queryModel = await compile_1.Malloy.compile({
604
608
  ...compilable,
605
- urlReader,
606
- connections,
607
- model,
609
+ urlReader: this.runtime.urlReader,
610
+ connections: this.runtime.connections,
611
+ model: await this.getModel(),
608
612
  refreshSchemaCache,
609
613
  noThrowOnError,
610
- importBaseURL: options === null || options === void 0 ? void 0 : options.importBaseURL,
611
- testEnvironment: options === null || options === void 0 ? void 0 : options.testEnvironment,
612
- ...this.compileQueryOptions,
614
+ importBaseURL,
615
+ testEnvironment: testEnvironment || this.runtime.isTestRuntime,
616
+ ...compileQueryOptions,
613
617
  });
614
618
  return queryModel.preparedQuery;
615
- });
619
+ }, compileQueryOptions);
616
620
  }
617
621
  /**
618
622
  * Load a Malloy query whose text comes from an untrusted source — an
@@ -1,5 +1,6 @@
1
1
  import type { SQLSourceRequest } from '../lang/translate-response';
2
2
  import type { MalloyQueryData, QueryRunStats, SQLSourceDef, StructDef, TableSourceDef } from '../model/malloy_types';
3
+ import type { QueryMetadata } from '../query_metadata';
3
4
  import type { RunSQLOptions } from '../run_sql_options';
4
5
  import type { Connection, FetchSchemaOptions, PersistSQLResults, PooledConnection, StreamingConnection } from './types';
5
6
  export interface SchemaFound<T extends StructDef> {
@@ -36,6 +37,28 @@ export declare abstract class BaseConnection implements Connection {
36
37
  error: string;
37
38
  structDef?: undefined;
38
39
  }>;
40
+ /**
41
+ * The validated bag, or undefined when there is nothing to apply. For
42
+ * connectors with a native key-value mechanism, e.g. BigQuery job labels.
43
+ */
44
+ protected queryMetadataBag(meta: QueryMetadata | undefined): QueryMetadata | undefined;
45
+ /**
46
+ * The metadata as a single SQL comment line, empty when there is nothing to
47
+ * apply. For a connector which has to place the comment itself rather than
48
+ * simply prepending it — see {@link sqlWithQueryMetadata}.
49
+ *
50
+ * ```
51
+ * -- NAME1="val1" NAME2="val2"
52
+ * ```
53
+ */
54
+ protected queryMetadataComment(meta: QueryMetadata | undefined): string;
55
+ /**
56
+ * `sql` with the metadata prepended as a leading comment, or `sql` unchanged
57
+ * when there is nothing to apply. For connectors with no native per-query
58
+ * mechanism. The contract guarantees the rendered comment is safe: no
59
+ * embedded quote, no newline.
60
+ */
61
+ protected sqlWithQueryMetadata(sql: string, meta: QueryMetadata | undefined): string;
39
62
  isPool(): this is PooledConnection;
40
63
  canPersist(): this is PersistSQLResults;
41
64
  canStream(): this is StreamingConnection;
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.BaseConnection = void 0;
8
8
  const sql_block_1 = require("../model/sql_block");
9
+ const query_metadata_1 = require("../query_metadata");
9
10
  const validate_table_path_1 = require("./validate_table_path");
10
11
  class BaseConnection {
11
12
  constructor() {
@@ -72,6 +73,49 @@ class BaseConnection {
72
73
  }
73
74
  return { error: 'Unknown schema fetch error' };
74
75
  }
76
+ /*
77
+ * Applying `RunSQLOptions.queryMetadata`. A connector uses whichever of these
78
+ * fits its backend: a native key-value mechanism takes the bag, everything
79
+ * else prepends the comment. They validate, so a bag that violates the
80
+ * contract throws rather than reaching the database in some mangled form.
81
+ */
82
+ /**
83
+ * The validated bag, or undefined when there is nothing to apply. For
84
+ * connectors with a native key-value mechanism, e.g. BigQuery job labels.
85
+ */
86
+ queryMetadataBag(meta) {
87
+ if (meta === undefined)
88
+ return undefined;
89
+ (0, query_metadata_1.validateQueryMetadata)(meta);
90
+ return Object.keys(meta).length > 0 ? meta : undefined;
91
+ }
92
+ /**
93
+ * The metadata as a single SQL comment line, empty when there is nothing to
94
+ * apply. For a connector which has to place the comment itself rather than
95
+ * simply prepending it — see {@link sqlWithQueryMetadata}.
96
+ *
97
+ * ```
98
+ * -- NAME1="val1" NAME2="val2"
99
+ * ```
100
+ */
101
+ queryMetadataComment(meta) {
102
+ const bag = this.queryMetadataBag(meta);
103
+ if (bag === undefined)
104
+ return '';
105
+ const rendered = Object.keys(bag)
106
+ .map(key => `${key}="${bag[key]}"`)
107
+ .join(' ');
108
+ return `-- ${rendered}\n`;
109
+ }
110
+ /**
111
+ * `sql` with the metadata prepended as a leading comment, or `sql` unchanged
112
+ * when there is nothing to apply. For connectors with no native per-query
113
+ * mechanism. The contract guarantees the rendered comment is safe: no
114
+ * embedded quote, no newline.
115
+ */
116
+ sqlWithQueryMetadata(sql, meta) {
117
+ return this.queryMetadataComment(meta) + sql;
118
+ }
75
119
  isPool() {
76
120
  return false;
77
121
  }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export type { Overlay, ConfigOverlays } from './api/foundation';
10
10
  export type { FilesystemContext, MalloyConfigOptions } from './api/foundation';
11
11
  export type { RuntimeContext } from './api/foundation';
12
12
  export type { QueryOptionsReader, RunSQLOptions } from './run_sql_options';
13
+ export type { QueryMetadata } from './query_metadata';
13
14
  export type { EventStream, ModelString, ModelURL, QueryString, QueryURL, URLReader, InvalidationKey, } from './runtime_types';
14
15
  export type { Connection, ConnectionConfig, ConnectionParameterValue, FetchSchemaOptions, InfoConnection, LookupConnection, PersistSQLResults, PooledConnection, TestableConnection, StreamingConnection, } from './connection/types';
15
16
  export { registerConnectionType, getConnectionProperties, getConnectionTypeDisplayName, getRegisteredConnectionTypes, createConnectionsFromConfig, } from './connection/registry';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Dialect-agnostic metadata attached per query to the statements a connection
3
+ * issues, for the backend's own bookkeeping (cost attribution, workload
4
+ * classification, tracing): a small bag of string-valued properties.
5
+ *
6
+ * Each connector applies it through a per-query mechanism — Snowflake's
7
+ * per-statement `QUERY_TAG` and BigQuery's per-job labels natively, others as a
8
+ * leading SQL comment. It never affects query results or data identity.
9
+ *
10
+ * This is the only name in this file which is public API; a connector reaches
11
+ * the machinery for applying a bag through `BaseConnection`.
12
+ */
13
+ export type QueryMetadata = Record<string, string>;
14
+ /** The ways `meta` violates the contract (empty = conforming). */
15
+ export declare function queryMetadataProblems(meta: QueryMetadata): string[];
16
+ /** Validate query metadata, throwing if the bag is invalid. */
17
+ export declare function validateQueryMetadata(meta: QueryMetadata): void;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright Contributors to the Malloy project
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.queryMetadataProblems = queryMetadataProblems;
8
+ exports.validateQueryMetadata = validateQueryMetadata;
9
+ // Contract for a bag that is usable across every backend and always safe to
10
+ // render (including into the `-- NAME="value"` comment form):
11
+ // - names: ASCII alphanumerics and underscore
12
+ // - values: printable ASCII, excluding the double-quote
13
+ // - a small number of properties
14
+ // Connectors may transform within these rules (e.g. BigQuery lowercases keys).
15
+ const QUERY_METADATA_MAX_KEY_LENGTH = 128;
16
+ const QUERY_METADATA_MAX_VALUE_LENGTH = 256;
17
+ const QUERY_METADATA_MAX_PROPERTIES = 20;
18
+ const KEY_RE = /^[A-Za-z0-9_]+$/;
19
+ // Printable ASCII (0x20–0x7e), excluding the double-quote (0x22) that would
20
+ // break the `NAME="value"` comment form. Also excludes control characters.
21
+ function isValidValue(s) {
22
+ for (let i = 0; i < s.length; i++) {
23
+ const code = s.charCodeAt(i);
24
+ if (code < 0x20 || code > 0x7e || code === 0x22)
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ /** The ways `meta` violates the contract (empty = conforming). */
30
+ function queryMetadataProblems(meta) {
31
+ const problems = [];
32
+ const keys = Object.keys(meta);
33
+ if (keys.length > QUERY_METADATA_MAX_PROPERTIES) {
34
+ problems.push(`more than ${QUERY_METADATA_MAX_PROPERTIES} properties`);
35
+ }
36
+ for (const key of keys) {
37
+ if (!KEY_RE.test(key)) {
38
+ problems.push(`property name '${key}' must be ASCII alphanumerics and underscore`);
39
+ }
40
+ if (key.length > QUERY_METADATA_MAX_KEY_LENGTH) {
41
+ problems.push(`property name '${key}' exceeds ${QUERY_METADATA_MAX_KEY_LENGTH} characters`);
42
+ }
43
+ const value = meta[key];
44
+ if (!isValidValue(value)) {
45
+ problems.push(`property '${key}' value contains characters outside printable ASCII (or ")`);
46
+ }
47
+ if (value.length > QUERY_METADATA_MAX_VALUE_LENGTH) {
48
+ problems.push(`property '${key}' value exceeds ${QUERY_METADATA_MAX_VALUE_LENGTH} characters`);
49
+ }
50
+ }
51
+ return problems;
52
+ }
53
+ /** Validate query metadata, throwing if the bag is invalid. */
54
+ function validateQueryMetadata(meta) {
55
+ const problems = queryMetadataProblems(meta);
56
+ if (problems.length > 0) {
57
+ throw new Error(`Invalid query metadata: ${problems.join('; ')}`);
58
+ }
59
+ }
60
+ //# sourceMappingURL=query_metadata.js.map
@@ -1,5 +1,12 @@
1
+ import type { QueryMetadata } from './query_metadata';
1
2
  export interface RunSQLOptions {
2
3
  rowLimit?: number;
3
4
  abortSignal?: AbortSignal;
5
+ /**
6
+ * Optional per-query metadata (a small bag of string-valued properties)
7
+ * attached to the query for the backend's own bookkeeping — applied by the
8
+ * connector according to its capabilities. See {@link QueryMetadata}.
9
+ */
10
+ queryMetadata?: QueryMetadata;
4
11
  }
5
12
  export type QueryOptionsReader = RunSQLOptions | (() => RunSQLOptions);
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.425";
1
+ export declare const MALLOY_VERSION = "0.0.426";
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MALLOY_VERSION = void 0;
4
4
  // generated with 'generate-version-file' script; do not edit manually
5
- exports.MALLOY_VERSION = '0.0.425';
5
+ exports.MALLOY_VERSION = '0.0.426';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.425",
3
+ "version": "0.0.426",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -51,9 +51,9 @@
51
51
  "generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
52
52
  },
53
53
  "dependencies": {
54
- "@malloydata/malloy-filter": "0.0.425",
55
- "@malloydata/malloy-interfaces": "0.0.425",
56
- "@malloydata/malloy-tag": "0.0.425",
54
+ "@malloydata/malloy-filter": "0.0.426",
55
+ "@malloydata/malloy-interfaces": "0.0.426",
56
+ "@malloydata/malloy-tag": "0.0.426",
57
57
  "@noble/hashes": "^1.8.0",
58
58
  "antlr4ts": "^0.5.0-alpha.4",
59
59
  "assert": "^2.0.0",