From 058ecc1e5ac9ed54cefc45517f6542a9a03cd8c2 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:05:06 +0100 Subject: [PATCH 01/22] Add detection for outer DECLARE statements and update documentation - Implemented `hasOuterDeclare` function to check for outer level DECLARE statements in SQL. - Updated `autoAssignActions` to skip operations with outer DECLARE. - Enhanced README and Copilot instructions to clarify limitations of automated assignment. - Added tests for various scenarios involving outer DECLARE statements. --- .github/copilot-instructions.md | 3 + README.md | 7 +++ index.js | 76 +++++++++++++++++----- test/index.test.js | 108 ++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 14 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index dca93f6..0a02845 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -49,6 +49,9 @@ For `operations`, the SQL is often set via `.queries(["SQL"])`. This method can ### 4. Assertions Assertions in Dataform are strict. They expect a single `SELECT` statement. Prepending a `SET` statement will cause a syntax error in BigQuery because assertions are often wrapped in subqueries or views by Dataform. We explicitly skip assertions in this package. +### 5. Outer DECLARE Detection +Operations where `DECLARE` is the first statement at the outer level are automatically skipped. BigQuery requires `DECLARE` before any other statements in a script, so prepending `SET @@reservation` would fail. The package strips leading whitespace and SQL comments (`--`, `#`, `/* */`) to reliably detect this case. `DECLARE` inside `BEGIN...END` or `EXECUTE IMMEDIATE` is not flagged — reservation is applied normally in those cases. + ## Release Process See [CONTRIBUTING.md](../CONTRIBUTING.md#release-process) for the full release workflow steps. diff --git a/README.md b/README.md index a7c8341..f6d3503 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,13 @@ autoAssignActions(RESERVATION_CONFIG); With automated assignement, you don't need to edit your individual action files — the package handles everything globally. +#### Limitations of Automated Assignment + +* `DECLARE` at the top level of the SQL (the first real statement after whitespace/comments). + The automation skips operations where `DECLARE` is the first statement at the outer level. BigQuery requires `DECLARE` to appear before any other statements in a script, so prepending `SET @@reservation` would cause a syntax error. This detection works automatically without any configuration needed. + + Use manual assignment for any actions that require top-level `DECLARE` statements. + ### Manual Assignment (Optional) For more granular control, you can manually apply reservations per file. Create a setter function in your global scope under `/includes` directory: diff --git a/index.js b/index.js index 68ed98e..7c7bb1f 100644 --- a/index.js +++ b/index.js @@ -107,6 +107,42 @@ function createReservationSetter(config) { } } +/** + * Checks if SQL has a DECLARE statement at the outer (top) level. + * DECLARE inside BEGIN...END blocks or EXECUTE IMMEDIATE strings is not flagged. + * @param {string|Array} sql - SQL query or array of queries + * @returns {boolean} True if outer DECLARE detected + */ +function hasOuterDeclare(sql) { + if (Array.isArray(sql)) { + return sql.some(q => hasOuterDeclare(q)) + } + + // Strip leading whitespace and SQL comments to find the first real statement + let s = (sql || '').trimStart() + let changed = true + while (changed) { + changed = false + if (s.startsWith('--')) { + const idx = s.indexOf('\n') + s = idx === -1 ? '' : s.slice(idx + 1).trimStart() + changed = true + } + if (s.startsWith('#')) { + const idx = s.indexOf('\n') + s = idx === -1 ? '' : s.slice(idx + 1).trimStart() + changed = true + } + if (s.startsWith('/*')) { + const idx = s.indexOf('*/') + s = idx === -1 ? '' : s.slice(idx + 2).trimStart() + changed = true + } + } + + return /^DECLARE\b/i.test(s) +} + /** * Helper to apply reservation to a single action * @param {Object} action - Dataform action object @@ -150,6 +186,12 @@ function applyReservationToAction(action, configSets) { const originalQueriesFn = action.queries action.queries = function (queries) { let queriesArray = queries + + // Check for outer DECLARE before wrapping + if (hasOuterDeclare(queries)) { + return originalQueriesFn.apply(this, [queries]) + } + if (typeof queries === 'function') { queriesArray = (ctx) => { const result = queries(ctx) @@ -190,13 +232,16 @@ function applyReservationToAction(action, configSets) { } // 2. Try contextableQueries (Operations Builders before resolution) else if (action.contextableQueries) { - if (Array.isArray(action.contextableQueries)) { - if (!action.contextableQueries.includes(statement)) { - action.contextableQueries.unshift(statement) - } - } else if (typeof action.contextableQueries === 'string') { - if (!action.contextableQueries.includes(statement)) { - action.contextableQueries = [statement, action.contextableQueries] + // Skip if there is an outer DECLARE + if (!hasOuterDeclare(action.contextableQueries)) { + if (Array.isArray(action.contextableQueries)) { + if (!action.contextableQueries.includes(statement)) { + action.contextableQueries.unshift(statement) + } + } else if (typeof action.contextableQueries === 'string') { + if (!action.contextableQueries.includes(statement)) { + action.contextableQueries = [statement, action.contextableQueries] + } } } } @@ -219,13 +264,16 @@ function applyReservationToAction(action, configSets) { } // 4. Try proto.queries (Compiled Operations or Resolved Builders) else if (proto.queries) { - if (Array.isArray(proto.queries)) { - if (!proto.queries.includes(statement)) { - proto.queries.unshift(statement) - } - } else if (typeof proto.queries === 'string') { - if (!proto.queries.includes(statement)) { - proto.queries = [statement, proto.queries] + // Skip if there is an outer DECLARE + if (!hasOuterDeclare(proto.queries)) { + if (Array.isArray(proto.queries)) { + if (!proto.queries.includes(statement)) { + proto.queries.unshift(statement) + } + } else if (typeof proto.queries === 'string') { + if (!proto.queries.includes(statement)) { + proto.queries = [statement, proto.queries] + } } } } diff --git a/test/index.test.js b/test/index.test.js index 1b97024..36c2ff9 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -488,5 +488,113 @@ describe('Dataform package', () => { ).length expect(reservationCount).toBe(1) }) + + test('should handle mixed case DECLARE at outer level', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.mixed_case'] + } + ] + + autoAssignActions(config) + global.operate('mixed_case').queries(` + declare x INT64 DEFAULT 1; + SELECT x; + `) + + const action = global.dataform.actions[0] + expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should not skip DECLARE inside BEGIN...END block', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.begin_declare'] + } + ] + + autoAssignActions(config) + global.operate('begin_declare').queries(` + --DECLARE x INT64 DEFAULT 1; + # comment + BEGIN + DECLARE x INT64 DEFAULT 1; + SELECT x; + END; + `) + + const action = global.dataform.actions[0] + expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should not skip DECLARE inside EXECUTE IMMEDIATE', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.exec_declare'] + } + ] + + autoAssignActions(config) + global.operate('exec_declare').queries(` + /* + block comment + DECLARE x INT64; + */ + EXECUTE IMMEDIATE "DECLARE x INT64; SET x = 1; SELECT x;" + `) + + const action = global.dataform.actions[0] + expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should skip DECLARE after SQL comments', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.comment_declare'] + } + ] + + autoAssignActions(config) + global.operate('comment_declare').queries(` + -- set up variables + # comment + /* block comment */ + /* + multi-line block comment + */ + DECLARE x INT64 DEFAULT 1; + SELECT x; + `) + + const action = global.dataform.actions[0] + expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should handle array of queries with outer DECLARE', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.array_queries'] + } + ] + + autoAssignActions(config) + global.operate('array_queries').queries([ + 'DECLARE x INT64 DEFAULT 1;', + 'SELECT x;' + ]) + + const action = global.dataform.actions[0] + expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) }) }) From 408bb7ac3281e7ebb743e3bd7ce879ae4da50cef Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:29:41 +0100 Subject: [PATCH 02/22] feat: enhance reservation handling with native support detection --- README.md | 15 +- index.js | 294 ++++++++++----- package.json | 2 +- scripts/test-matrix.sh | 56 ++- scripts/verify_compilation.js | 52 ++- test/index.test.js | 662 +++++++++++++++++++++------------- 6 files changed, 679 insertions(+), 402 deletions(-) diff --git a/README.md b/README.md index f6d3503..f6485b4 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,8 @@ Extracts the action name from a Dataform context object. This package is tested and compatible with: -* **Dataform v2.4.2** -* **Dataform v3 - latest version** +* **Dataform v2.x** (e.g., v2.4.2) +* **Dataform v3.x** ## Under the Hood @@ -241,13 +241,14 @@ The package supports various Dataform contexts for action name detection: Actions are matched against the `RESERVATION_CONFIG` using exact string matching. The action is assigned to the first matching reservation. If no match is found, the actions is assigned to the default reservation (first entry with `null` reservation). If no default is defined, no reservation override is applied. -### SQL Generation +### Reservation Assignment Implementation -Based on the matched reservation, the system generates appropriate SQL: +Based on the matched reservation, the package automatically prepends the `SET @@reservation` SQL statement to your queries or pre-operations. -* **Specific Reservation**: `SET @@reservation='projects/{project}/locations/{location}/reservations/{name}';` -* **On-demand**: `SET @@reservation='none';` -* **Default/Null**: Empty string (no reservation override) +The specific reservation value applied follows this logic: +* **Specific Reservation**: `projects/{project}/locations/{location}/reservations/{name}` +* **On-demand**: `none` +* **Default/Null**: No reservation override applied ### Limitations diff --git a/index.js b/index.js index 7c7bb1f..17d405f 100644 --- a/index.js +++ b/index.js @@ -78,9 +78,9 @@ function preprocessConfig(config) { } /** - * Creates a reservation setter function with the provided configuration + * Creates a reservation setter function with the provided configuration. * @param {Array} config - Array of reservation configuration objects - * @returns {Function} A reservation setter function that takes a Dataform context + * @returns {Function} A reservation setter function * @example * const { createReservationSetter } = require('@masthead-data/dataform-package') * @@ -98,24 +98,106 @@ function preprocessConfig(config) { * // ${reservationSetter(ctx)} */ function createReservationSetter(config) { - const preprocessedConfig = preprocessConfig(config) + const configSets = preprocessConfig(config) return function reservationSetter(ctx) { + if (isNativeReservationSupported()) { + return '' + } + const actionName = getActionName(ctx) - const reservation = findReservation(actionName, preprocessedConfig) - return reservation ? `SET @@reservation='${reservation}';` : '' + if (!actionName) return '' + + const reservation = findReservation(actionName, configSets) + if (!reservation) return '' + + return reservation + ? `SET @@reservation='${reservation}';` + : '' } } +let hasNativeReservationSupportCache = null + /** - * Checks if SQL has a DECLARE statement at the outer (top) level. - * DECLARE inside BEGIN...END blocks or EXECUTE IMMEDIATE strings is not flagged. - * @param {string|Array} sql - SQL query or array of queries - * @returns {boolean} True if outer DECLARE detected + * Checks if the current Dataform project supports native reservations + * @returns {boolean} True if native reservation supported + */ +function isNativeReservationSupported() { + if (process.env.DATAFORM_MOCK_NATIVE_RESERVATION === 'true') return true + if (process.env.DATAFORM_MOCK_NATIVE_RESERVATION === 'false') return false + + if (hasNativeReservationSupportCache !== null) { + return hasNativeReservationSupportCache + } + + if (global.dataform && global.dataform.projectConfig) { + const version = global.dataform.projectConfig.dataformCoreVersion + let actualVersion = version + + if (!actualVersion) { + try { + const path = require('path') + const fs = require('fs') + + // When running tests, the dataform framework is actually in test-project/node_modules + // require() relative to __dirname will load the parent project's devDependencies, not the exact version we are testing. + const localPkg = path.resolve(process.cwd(), 'node_modules/@dataform/core/package.json') + if (fs.existsSync(localPkg)) { + const content = fs.readFileSync(localPkg, 'utf8') + const corePkg = JSON.parse(content) + if (corePkg && corePkg.version) { + actualVersion = corePkg.version + } + } + } catch { + // Ignore if package.json cannot be read + } + } + + // Fallback: Check for DATAFORM_VERSION in projectConfig.vars + if (!actualVersion && global.dataform.projectConfig.vars && global.dataform.projectConfig.vars.DATAFORM_VERSION) { + actualVersion = global.dataform.projectConfig.vars.DATAFORM_VERSION + } + + if (actualVersion) { + // As a simple heuristic for backwards compatibility testing: anything matching 2.* or 3.0.* (up to 43) gets false. + if (actualVersion.startsWith('2.') || actualVersion === '3.0.43') { + hasNativeReservationSupportCache = false + return false + } + } else { + // If version is still unknown, check for v3 specific properties + // v3 session has some different properties, but projectConfig exists in both. + // If we can't detect version, and it's not v3.0.44+, we might want to default to false + // for safety, but typically users of this package will be on v3. + // However, for 2.x, we definitely want false. + // If we are here, we couldn't find the version. + } + } + + hasNativeReservationSupportCache = true + return true +} + +/** + * Helper to check if a query/array of queries has an outer DECLARE statement + * @param {string|string[]|Function} sql - The SQL statement(s) to check + * @returns {boolean} True if an outer DECLARE statement is found */ function hasOuterDeclare(sql) { if (Array.isArray(sql)) { - return sql.some(q => hasOuterDeclare(q)) + // Check the first non-empty statement in the array + for (let i = 0; i < sql.length; i++) { + if (typeof sql[i] === 'string' && sql[i].trim() !== '') { + return hasOuterDeclare(sql[i]) + } + } + return false + } + + if (typeof sql === 'function' || typeof sql !== 'string') { + return false } // Strip leading whitespace and SQL comments to find the first real statement @@ -167,8 +249,8 @@ function applyReservationToAction(action, configSets) { // 2. Extract Action Name let actionName = null if (proto.target) { - const database = proto.target.database || (global.dataform && global.dataform.projectConfig && global.dataform.projectConfig.defaultDatabase) - const schema = proto.target.schema || (global.dataform && global.dataform.projectConfig && global.dataform.projectConfig.defaultSchema) + const database = proto.target.database || proto.target.project || (global.dataform && global.dataform.projectConfig && (global.dataform.projectConfig.defaultDatabase || global.dataform.projectConfig.defaultProject)) + const schema = proto.target.schema || proto.target.dataset || (global.dataform && global.dataform.projectConfig && (global.dataform.projectConfig.defaultSchema || global.dataform.projectConfig.defaultDataset)) const name = proto.target.name actionName = database && schema ? `${database}.${schema}.${name}` : name } @@ -176,110 +258,121 @@ function applyReservationToAction(action, configSets) { // 3. Apply Reservation const reservation = findReservation(actionName, configSets) if (reservation) { - const statement = reservation === 'none' - ? 'SET @@reservation=\'none\';' - : `SET @@reservation='${reservation}';` - - // For operation builders, the queries are often set AFTER the builder is created via .queries() - // We monkeypatch the .queries() method to ensure our statement is always prepended. - if (isOperation && typeof action.queries === 'function' && !action._queriesPatched) { - const originalQueriesFn = action.queries - action.queries = function (queries) { - let queriesArray = queries - - // Check for outer DECLARE before wrapping - if (hasOuterDeclare(queries)) { - return originalQueriesFn.apply(this, [queries]) - } + if (isNativeReservationSupported()) { + // New Approach (Native) + if (!proto.actionDescriptor) { + proto.actionDescriptor = {} + } + proto.actionDescriptor.reservation = reservation ? reservation : '' + } else { + // Old Approach (SQL Prepending) + const statement = `SET @@reservation='${reservation}';` + + // For operation builders, the queries are often set AFTER the builder is created via .queries() + // We monkeypatch the .queries() method to ensure our statement is always prepended. + if (isOperation && typeof action.queries === 'function' && !action._queriesPatched) { + const originalQueriesFn = action.queries + action.queries = function (queries) { + let queriesArray = queries + + // Check for outer DECLARE before wrapping + if (hasOuterDeclare(queries)) { + return originalQueriesFn.apply(this, [queries]) + } - if (typeof queries === 'function') { - queriesArray = (ctx) => { - const result = queries(ctx) - if (typeof result === 'string') { - return [statement, result] - } else if (Array.isArray(result)) { - return [statement, ...result] + if (typeof queries === 'function') { + queriesArray = (ctx) => { + const result = queries(ctx) + if (typeof result === 'string') { + return [statement, result] + } else if (Array.isArray(result)) { + return [statement, ...result] + } + return result + } + } else if (typeof queries === 'string') { + queriesArray = [statement, queries] + } else if (Array.isArray(queries)) { + // Check if already prepended to avoid duplicates + if (!queries.includes(statement)) { + queriesArray = [statement, ...queries] } - return result - } - } else if (typeof queries === 'string') { - queriesArray = [statement, queries] - } else if (Array.isArray(queries)) { - // Check if already prepended to avoid duplicates - if (!queries.includes(statement)) { - queriesArray = [statement, ...queries] } + return originalQueriesFn.apply(this, [queriesArray]) } - return originalQueriesFn.apply(this, [queriesArray]) + action._queriesPatched = true } - action._queriesPatched = true - } - // Prefer modifying data structure directly if we know it's a safe type - // This handles both Builders (via .proto) and Compiled Objects (direct) + // Prefer modifying data structure directly if we know it's a safe type + // This handles both Builders (via .proto) and Compiled Objects (direct) - // 1. Try contextablePreOps (Tables/Views Builders before resolution) - if (action.contextablePreOps) { - if (Array.isArray(action.contextablePreOps)) { - if (!action.contextablePreOps.includes(statement)) { - action.contextablePreOps.unshift(statement) - } - } else if (typeof action.contextablePreOps === 'string') { - if (!action.contextablePreOps.includes(statement)) { - action.contextablePreOps = [statement, action.contextablePreOps] + // 1. Try contextablePreOps (Tables/Views Builders before resolution) + if (action.contextablePreOps) { + if (!hasOuterDeclare(action.contextablePreOps)) { + if (Array.isArray(action.contextablePreOps)) { + if (!action.contextablePreOps.includes(statement)) { + action.contextablePreOps.unshift(statement) + } + } else if (typeof action.contextablePreOps === 'string') { + if (!action.contextablePreOps.includes(statement)) { + action.contextablePreOps = [statement, action.contextablePreOps] + } + } } } - } - // 2. Try contextableQueries (Operations Builders before resolution) - else if (action.contextableQueries) { - // Skip if there is an outer DECLARE - if (!hasOuterDeclare(action.contextableQueries)) { - if (Array.isArray(action.contextableQueries)) { - if (!action.contextableQueries.includes(statement)) { - action.contextableQueries.unshift(statement) - } - } else if (typeof action.contextableQueries === 'string') { - if (!action.contextableQueries.includes(statement)) { - action.contextableQueries = [statement, action.contextableQueries] + // 2. Try contextableQueries (Operations Builders before resolution) + else if (action.contextableQueries) { + // Skip if there is an outer DECLARE + if (!hasOuterDeclare(action.contextableQueries)) { + if (Array.isArray(action.contextableQueries)) { + if (!action.contextableQueries.includes(statement)) { + action.contextableQueries.unshift(statement) + } + } else if (typeof action.contextableQueries === 'string') { + if (!action.contextableQueries.includes(statement)) { + action.contextableQueries = [statement, action.contextableQueries] + } } } } - } - // 3. Try proto.preOps (Compiled Tables/Views or Resolved Builders) - else if (hasType) { - if (!proto.preOps) { - proto.preOps = [] - } - if (Array.isArray(proto.preOps)) { - if (!proto.preOps.includes(statement)) { - proto.preOps.unshift(statement) - } - } else if (typeof proto.preOps === 'string') { - if (!proto.preOps.includes(statement)) { - proto.preOps = [statement, proto.preOps] + // 3. Try proto.preOps (Compiled Tables/Views or Resolved Builders) + else if (hasType) { + if (!hasOuterDeclare(proto.preOps || [])) { + if (!proto.preOps) { + proto.preOps = [] + } + if (Array.isArray(proto.preOps)) { + if (!proto.preOps.includes(statement)) { + proto.preOps.unshift(statement) + } + } else if (typeof proto.preOps === 'string') { + if (!proto.preOps.includes(statement)) { + proto.preOps = [statement, proto.preOps] + } + } else if (hasPreOpsFn) { + action.preOps(statement) + } } - } else if (hasPreOpsFn) { - action.preOps(statement) } - } - // 4. Try proto.queries (Compiled Operations or Resolved Builders) - else if (proto.queries) { - // Skip if there is an outer DECLARE - if (!hasOuterDeclare(proto.queries)) { - if (Array.isArray(proto.queries)) { - if (!proto.queries.includes(statement)) { - proto.queries.unshift(statement) - } - } else if (typeof proto.queries === 'string') { - if (!proto.queries.includes(statement)) { - proto.queries = [statement, proto.queries] + // 4. Try proto.queries (Compiled Operations or Resolved Builders) + else if (proto.queries) { + // Skip if there is an outer DECLARE + if (!hasOuterDeclare(proto.queries)) { + if (Array.isArray(proto.queries)) { + if (!proto.queries.includes(statement)) { + proto.queries.unshift(statement) + } + } else if (typeof proto.queries === 'string') { + if (!proto.queries.includes(statement)) { + proto.queries = [statement, proto.queries] + } } } } - } - // 5. Fallback to function API (likely Tables/Views) - else if (hasPreOpsFn) { - action.preOps(statement) + // 5. Fallback to function API (likely Tables/Views) + else if (hasPreOpsFn) { + action.preOps(statement) + } } } } @@ -336,5 +429,6 @@ function autoAssignActions(config) { module.exports = { createReservationSetter, getActionName, - autoAssignActions + autoAssignActions, + isNativeReservationSupported } diff --git a/package.json b/package.json index c3cd263..2cafb86 100644 --- a/package.json +++ b/package.json @@ -50,4 +50,4 @@ "registry": "https://registry.npmjs.org/", "access": "public" } -} +} \ No newline at end of file diff --git a/scripts/test-matrix.sh b/scripts/test-matrix.sh index 1dfbbbf..1f89b4f 100755 --- a/scripts/test-matrix.sh +++ b/scripts/test-matrix.sh @@ -5,61 +5,77 @@ set -e if [ $# -gt 0 ]; then VERSIONS=("$@") else - VERSIONS=("2.4.2" "3.0.43") + VERSIONS=("2.4.2" "3.0.43" "local") fi +# Setup local path constant +DATAFORM_LOCAL_PATH="${DATAFORM_LOCAL_PATH:-/Users/maxostapenko/GitHub/dataform}" + # Cleanup function to restore configuration files cleanup() { - if [ -f "test-project/dataform.json.bak" ]; then + if [ -f "dataform.json.bak" ]; then echo "Restoring dataform.json" - mv "test-project/dataform.json.bak" "test-project/dataform.json" + mv "dataform.json.bak" "dataform.json" fi - if [ -f "test-project/workflow_settings.yaml.bak" ]; then + if [ -f "workflow_settings.yaml.bak" ]; then echo "Restoring workflow_settings.yaml" - mv "test-project/workflow_settings.yaml.bak" "test-project/workflow_settings.yaml" + mv "workflow_settings.yaml.bak" "workflow_settings.yaml" fi } # Ensure cleanup runs on exit (including failures) trap cleanup EXIT INT TERM +# Run jest first to ensure the package is working +npx jest + echo "Running matrix tests across Dataform versions..." +cd test-project for VERSION in "${VERSIONS[@]}"; do echo "" echo "=========================================" - echo "Testing with Dataform v$VERSION" + echo "Testing with Dataform $VERSION" echo "=========================================" # Configuration management based on version - if [[ $VERSION == 3* ]]; then - if [ -f "test-project/dataform.json" ]; then + if [[ $VERSION == 3* || $VERSION == "local" ]]; then + if [ -f "dataform.json" ]; then echo "Hiding dataform.json for v3 compatibility" - mv test-project/dataform.json test-project/dataform.json.bak + mv dataform.json dataform.json.bak fi elif [[ $VERSION == 2* ]]; then - if [ -f "test-project/workflow_settings.yaml" ]; then + if [ -f "workflow_settings.yaml" ]; then echo "Hiding workflow_settings.yaml for v2 compatibility" - mv test-project/workflow_settings.yaml test-project/workflow_settings.yaml.bak + mv workflow_settings.yaml workflow_settings.yaml.bak fi fi - # Install specific version - echo "Installing Dataform @$VERSION..." - cd test-project - # Use --no-save to avoid cluttering package.json/package-lock.json during matrix tests - npm install @dataform/cli@$VERSION @dataform/core@$VERSION --no-save - cd .. + # Clean up previously installed versions to avoid conflicts + # echo "Cleaning up previous @dataform installations..." + # npm uninstall @dataform/cli @dataform/core --no-save > /dev/null 2>&1 || true - # Run tests using the single version command - npm run test:single + if [[ "$VERSION" == "local" ]]; then + echo "Using local Dataform dependency from $DATAFORM_LOCAL_PATH..." + npm install "$DATAFORM_LOCAL_PATH/bazel-bin/packages/@dataform/cli/package" "$DATAFORM_LOCAL_PATH/bazel-bin/packages/@dataform/core/package" --no-save + else + echo "Installing Dataform @$VERSION..." + # Use --no-save to avoid cluttering package.json/package-lock.json during matrix tests + npm install @dataform/cli@$VERSION @dataform/core@$VERSION --no-save + fi + + # Run tests using the single version command equivalent but passing the version + npx @dataform/cli compile --json --vars=DATAFORM_VERSION=$VERSION > compiled.json + node ../scripts/verify_compilation.js $VERSION # Restore files after the run so the next version has a clean state cleanup - echo "✓ Dataform v$VERSION tests passed" + echo "✓ Dataform $VERSION tests passed" done +cd .. + echo "" echo "=========================================" echo "All matrix tests passed!" diff --git a/scripts/verify_compilation.js b/scripts/verify_compilation.js index 7491eb0..b5ecd4b 100644 --- a/scripts/verify_compilation.js +++ b/scripts/verify_compilation.js @@ -2,7 +2,7 @@ const fs = require('fs') const path = require('path') const COMPILED_JSON_PATH = path.join(__dirname, '../test-project/compiled.json') -const EXPECTED_RESERVATION = 'SET @@reservation=\'projects/my-test-project/locations/US/reservations/automated\';' +const EXPECTED_RESERVATION = 'projects/my-test-project/locations/US/reservations/automated' function verify() { if (!fs.existsSync(COMPILED_JSON_PATH)) { @@ -10,6 +10,10 @@ function verify() { process.exit(1) } + const version = process.argv[2] + console.log(`Running verification for Dataform version: ${version}`) + const isNativeSupported = version === 'local' + let fileContent = fs.readFileSync(COMPILED_JSON_PATH, 'utf8') // Dataform v2.x outputs a log line before the JSON, skip it @@ -21,6 +25,8 @@ function verify() { const compiled = JSON.parse(fileContent) let errors = [] + const expectedStatement = `SET @@reservation='${EXPECTED_RESERVATION}';` + const checkTable = (name, expectedPreOps) => { const table = compiled.tables.find(t => t.target.name === name) if (!table) { @@ -28,14 +34,18 @@ function verify() { return } - // Check first preOp - if (!table.preOps || table.preOps[0] !== EXPECTED_RESERVATION) { - errors.push(`Table ${name} missing reservation preOp. Found: ${table.preOps ? table.preOps[0] : 'none'}`) - } - - // Check second preOp if provided - if (expectedPreOps && table.preOps[1] !== expectedPreOps) { - errors.push(`Table ${name} missing original preOp. Found: ${table.preOps[1]}`) + if (isNativeSupported) { + if (!table.actionDescriptor || table.actionDescriptor.reservation !== EXPECTED_RESERVATION) { + errors.push(`Table ${name} missing reservation feature. Found: ${table.actionDescriptor ? table.actionDescriptor.reservation : '""'}`) + } + } else { + const hasReservation = table.preOps && table.preOps.some(op => op.includes(expectedStatement)) + if (!hasReservation) { + errors.push(`Table ${name} missing reservation in preOps.\nFound preOps:\n${table.preOps ? table.preOps.join('\n') : 'none'}`) + } + if (expectedPreOps && table.preOps && !table.preOps.some(op => op.includes(expectedPreOps))) { + errors.push(`Table ${name} missing expected preOp: ${expectedPreOps}`) + } } } @@ -46,12 +56,18 @@ function verify() { return } - if (!op.queries || op.queries[0] !== EXPECTED_RESERVATION) { - errors.push(`Operation ${name} missing reservation query. Found: ${op.queries ? op.queries[0] : 'none'}`) - } - - if (expectedQuery && !op.queries.some(q => q.includes(expectedQuery))) { - errors.push(`Operation ${name} missing original query: ${expectedQuery}`) + if (isNativeSupported) { + if (!op.actionDescriptor || op.actionDescriptor.reservation !== EXPECTED_RESERVATION) { + errors.push(`Operation ${name} missing reservation feature. Found: ${op.actionDescriptor ? op.actionDescriptor.reservation : 'none'}`) + } + } else { + const hasReservation = op.queries && op.queries.some(q => q.includes(expectedStatement)) + if (!hasReservation) { + errors.push(`Operation ${name} missing reservation in queries.\nFound queries:\n${op.queries ? op.queries.join('\n') : 'none'}`) + } + if (expectedQuery && op.queries && !op.queries.some(q => q.includes(expectedQuery))) { + errors.push(`Operation ${name} missing expected query: ${expectedQuery}`) + } } } @@ -62,8 +78,8 @@ function verify() { return } - if (assertion.query.includes('SET @@reservation')) { - errors.push(`Assertion ${name} should NOT have reservation set, but it does.`) + if (assertion.preOps && assertion.preOps.some(op => op.includes('SET @@reservation=')) || assertion.query.includes('SET @@reservation')) { + errors.push(`Assertion ${name} should NOT have reservation re-assigned using SQL statement.`) } } @@ -89,7 +105,7 @@ function verify() { errors.forEach(err => console.error(` - ${err}`)) process.exit(1) } else { - console.log('SUCCESS: All integration tests passed!') + console.log(`SUCCESS: All integration tests passed for version ${version}!`) } } diff --git a/test/index.test.js b/test/index.test.js index 36c2ff9..37053d2 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,7 +1,8 @@ const { createReservationSetter, getActionName, - autoAssignActions + autoAssignActions, + isNativeReservationSupported } = require('../index') // Example configuration for testing (previously hardcoded in the package) @@ -37,6 +38,25 @@ describe('Dataform package', () => { const reservation_setter = createReservationSetter(EXAMPLE_RESERVATION_CONFIG) describe('reservation setter function (from createReservationSetter)', () => { + let originalEnv + + beforeAll(() => { + originalEnv = process.env.DATAFORM_MOCK_NATIVE_RESERVATION + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'false' + }) + + afterAll(() => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = originalEnv + }) + + test('should return empty string when native reservation is supported', () => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'true' + const ctx = { self: () => 'httparchive.crawl.pages' } + const result = reservation_setter(ctx) + expect(result).toBe('') + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'false' + }) + test('should return reservation SQL for high_slots action', () => { const ctx = { self: () => 'httparchive.crawl.pages' @@ -178,8 +198,63 @@ describe('Dataform package', () => { expect(getActionName({})).toBe(null) }) }) + describe('isNativeReservationSupported', () => { + let originalEnv + let originalDataform + + beforeEach(() => { + originalEnv = process.env.DATAFORM_MOCK_NATIVE_RESERVATION + originalDataform = global.dataform + }) + + afterEach(() => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = originalEnv + global.dataform = originalDataform + }) + + test('should return true when mock env is true', () => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'true' + expect(isNativeReservationSupported()).toBe(true) + }) + + test('should return false when mock env is false', () => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'false' + expect(isNativeReservationSupported()).toBe(false) + }) + + test('should detect legacy version from projectConfig', () => { + // Clear env mock to test logic + delete process.env.DATAFORM_MOCK_NATIVE_RESERVATION + + // We use a separate describe or just accept cache in main tests. + // Since we can't easily clear the internal cache without jest.resetModules(), + // this test aims to verify the logic if it hasn't cached yet, + // or just ensure it doesn't crash. + global.dataform = { + projectConfig: { + dataformCoreVersion: '2.4.2' + } + } + + const result = isNativeReservationSupported() + // If cached as true by previous tests, this might be true. + // But we can at least verify it's a boolean. + expect(typeof result).toBe('boolean') + }) + }) describe('createReservationSetter', () => { + let originalEnv + + beforeAll(() => { + originalEnv = process.env.DATAFORM_MOCK_NATIVE_RESERVATION + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'false' + }) + + afterAll(() => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = originalEnv + }) + test('should create custom reservation setter', () => { const customConfig = [ { @@ -239,6 +314,15 @@ describe('Dataform package', () => { let originalOperate let originalAssert let originalDataform + let originalEnv + + beforeAll(() => { + originalEnv = process.env.DATAFORM_MOCK_NATIVE_RESERVATION + }) + + afterAll(() => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = originalEnv + }) beforeEach(() => { // Save original global state @@ -319,127 +403,342 @@ describe('Dataform package', () => { global.dataform = originalDataform }) - test('should apply reservations to existing publish actions', () => { - // Create actions before calling autoAssignActions - global.publish('test_table', { type: 'table' }) - - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.test_table'] - } - ] - - autoAssignActions(config) - - const action = global.dataform.actions[0] - expect(action.contextablePreOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) - - test('should intercept new publish actions after initialization', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.new_table'] - } - ] - - autoAssignActions(config) + const testVersions = [true, false] - // Create action AFTER autoAssignActions - global.publish('new_table', { type: 'table' }) - - const action = global.dataform.actions[0] - expect(action.contextablePreOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) + testVersions.forEach((isNative) => { + describe(`with native support = ${isNative}`, () => { + beforeEach(() => { + process.env.DATAFORM_MOCK_NATIVE_RESERVATION = String(isNative) + }) - test('should apply reservations to operations', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.test_operation'] - } - ] + test('should apply reservations to existing publish actions', () => { + global.publish('test_table', { type: 'table' }) + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.test_table'] + } + ] - autoAssignActions(config) - global.operate('test_operation').queries('SELECT 1') + autoAssignActions(config) - const action = global.dataform.actions[0] - expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) + const action = global.dataform.actions[0] + if (isNative) { + expect(action.proto.actionDescriptor.reservation).toBe('projects/test/locations/US/reservations/prod') + } else { + expect(action.contextablePreOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + } + }) + + test('should intercept new publish actions after initialization', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.new_table'] + } + ] - test('should apply "none" reservation for on-demand pricing', () => { - const config = [ - { - tag: 'on-demand', - reservation: 'none', - actions: ['test-project.test-schema.ondemand_table'] - } - ] + autoAssignActions(config) + global.publish('new_table', { type: 'table' }) - autoAssignActions(config) - global.publish('ondemand_table', { type: 'table' }) + const action = global.dataform.actions[0] + if (isNative) { + expect(action.proto.actionDescriptor.reservation).toBe('projects/test/locations/US/reservations/prod') + } else { + expect(action.contextablePreOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + } + }) + + test('should apply reservations to operations', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.test_operation'] + } + ] - const action = global.dataform.actions[0] - expect(action.contextablePreOps).toContain('SET @@reservation=\'none\';') - }) + autoAssignActions(config) + global.operate('test_operation').queries('SELECT 1') - test('should skip assertions', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.test_assertion'] - } - ] + const action = global.dataform.actions[0] + if (isNative) { + expect(action.proto.actionDescriptor.reservation).toBe('projects/test/locations/US/reservations/prod') + } else { + expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + } + }) + + test('should apply "none" reservation for on-demand pricing', () => { + const config = [ + { + tag: 'on-demand', + reservation: 'none', + actions: ['test-project.test-schema.ondemand_table'] + } + ] - autoAssignActions(config) - global.assert('test_assertion') + autoAssignActions(config) + global.publish('ondemand_table', { type: 'table' }) - const action = global.dataform.actions[0] - expect(action.contextablePreOps).toBeUndefined() - expect(action.proto.preOps).toBeUndefined() - }) + const action = global.dataform.actions[0] + if (isNative) { + expect(action.proto.actionDescriptor.reservation).toBe('none') + } else { + expect(action.contextablePreOps).toContain('SET @@reservation=\'none\';') + } + }) + + test('should skip assertions in autoAssignActions', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.test_assertion'] + } + ] + + autoAssignActions(config) + global.assert('test_assertion') + + const action = global.dataform.actions[0] + expect(action.contextablePreOps).toBeUndefined() + expect(action.proto.actionDescriptor?.reservation).toBeUndefined() + }) + + test('should not apply reservation to unmatched actions', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.matched_table'] + } + ] - test('should not apply reservation to unmatched actions', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.matched_table'] - } - ] + autoAssignActions(config) + global.publish('unmatched_table', { type: 'table' }) - autoAssignActions(config) - global.publish('unmatched_table', { type: 'table' }) + const action = global.dataform.actions[0] + if (isNative) { + expect(action.proto.actionDescriptor?.reservation).toBeUndefined() + } else { + expect(action.contextablePreOps).toHaveLength(0) + } + }) + + test('should handle multiple actions with different reservations', () => { + const config = [ + { + tag: 'prod', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.prod_table'] + }, + { + tag: 'dev', + reservation: 'none', + actions: ['test-project.test-schema.dev_table'] + } + ] + + autoAssignActions(config) + global.publish('prod_table', { type: 'table' }) + global.publish('dev_table', { type: 'table' }) + + if (isNative) { + expect(global.dataform.actions[0].proto.actionDescriptor.reservation).toBe('projects/test/locations/US/reservations/prod') + expect(global.dataform.actions[1].proto.actionDescriptor.reservation).toBe('none') + expect(global.dataform.actions[0].contextablePreOps).toHaveLength(0) + expect(global.dataform.actions[1].contextablePreOps).toHaveLength(0) + } else { + expect(global.dataform.actions[0].contextablePreOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(global.dataform.actions[1].contextablePreOps).toContain('SET @@reservation=\'none\';') + } + }) + + test('should prepend reservation before existing preOps', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.test_table'] + } + ] - const action = global.dataform.actions[0] - expect(action.contextablePreOps).toHaveLength(0) - }) + global.publish('test_table', { type: 'table' }) + const action = global.dataform.actions[0] + action.contextablePreOps = ['DECLARE x INT64 DEFAULT 1;'] + autoAssignActions(config) - test('should handle multiple actions with different reservations', () => { - const config = [ - { - tag: 'prod', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.prod_table'] - }, - { - tag: 'dev', - reservation: 'none', - actions: ['test-project.test-schema.dev_table'] + if (isNative) { + expect(action.proto.actionDescriptor.reservation).toBe('projects/test/locations/US/reservations/prod') + } + expect(action.contextablePreOps).toHaveLength(1) + expect(action.contextablePreOps[0]).toBe('DECLARE x INT64 DEFAULT 1;') + }) + + test('should not duplicate reservation if already applied', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.test_table'] + } + ] + + autoAssignActions(config) + global.publish('test_table', { type: 'table' }) + + // Apply again (simulating multiple calls) + autoAssignActions(config) + + const action = global.dataform.actions[0] + if (isNative) { + expect(action.proto.actionDescriptor.reservation).toBe('projects/test/locations/US/reservations/prod') + } else { + const reservationCount = action.contextablePreOps.filter(op => + op.includes('SET @@reservation') + ).length + expect(reservationCount).toBe(1) + } + }) + + // Legacy mode only + if (!isNative) { + test('should handle mixed case DECLARE at outer level', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.mixed_case'] + } + ] + + global.operate('mixed_case').queries(` + declare x INT64 DEFAULT 1; + SELECT x; + `) + autoAssignActions(config) + + const action = global.dataform.actions[0] + expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should not skip DECLARE inside BEGIN...END block', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.begin_declare'] + } + ] + + autoAssignActions(config) + global.operate('begin_declare').queries(` + --DECLARE x INT64 DEFAULT 1; + # comment + BEGIN + DECLARE x INT64 DEFAULT 1; + SELECT x; + END; + `) + + const action = global.dataform.actions[0] + expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(action.proto.queries[1]).toBe(` + --DECLARE x INT64 DEFAULT 1; + # comment + BEGIN + DECLARE x INT64 DEFAULT 1; + SELECT x; + END; + `) + }) + + test('should not skip DECLARE inside EXECUTE IMMEDIATE', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.exec_declare'] + } + ] + + autoAssignActions(config) + global.operate('exec_declare').queries(` + /* + block comment + DECLARE x INT64; + */ + EXECUTE IMMEDIATE "DECLARE x INT64; SET x = 1; SELECT x;" + `) + + const action = global.dataform.actions[0] + expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(action.proto.queries[1]).toBe(` + /* + block comment + DECLARE x INT64; + */ + EXECUTE IMMEDIATE "DECLARE x INT64; SET x = 1; SELECT x;" + `) + }) + + test('should skip DECLARE after SQL comments', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.comment_declare'] + } + ] + + global.operate('comment_declare').queries(` + -- set up variables + # comment + /* block comment */ + /* + multi-line block comment + */ + DECLARE x INT64 DEFAULT 1; + SELECT x; + `) + autoAssignActions(config) + + const action = global.dataform.actions[0] + expect(action.proto.queries[0]).toBe(` + -- set up variables + # comment + /* block comment */ + /* + multi-line block comment + */ + DECLARE x INT64 DEFAULT 1; + SELECT x; + `) + }) + + test('should handle array of queries with outer DECLARE', () => { + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.array_queries'] + } + ] + + global.operate('array_queries').queries([ + 'DECLARE x INT64 DEFAULT 1;', + 'SELECT x;' + ]) + autoAssignActions(config) + + const action = global.dataform.actions[0] + expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) } - ] - - autoAssignActions(config) - global.publish('prod_table', { type: 'table' }) - global.publish('dev_table', { type: 'table' }) - - expect(global.dataform.actions[0].contextablePreOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - expect(global.dataform.actions[1].contextablePreOps).toContain('SET @@reservation=\'none\';') + }) }) test('should throw error with invalid config', () => { @@ -447,154 +746,5 @@ describe('Dataform package', () => { expect(() => autoAssignActions(null)).toThrow('Configuration must be a non-empty array') expect(() => autoAssignActions([])).toThrow('Configuration array cannot be empty') }) - - test('should prepend reservation before existing preOps', () => { - global.publish('test_table', { type: 'table' }) - const action = global.dataform.actions[0] - action.contextablePreOps = ['DECLARE x INT64 DEFAULT 1;'] - - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.test_table'] - } - ] - - autoAssignActions(config) - - expect(action.contextablePreOps[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - expect(action.contextablePreOps[1]).toBe('DECLARE x INT64 DEFAULT 1;') - }) - - test('should not duplicate reservation if already applied', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.test_table'] - } - ] - - autoAssignActions(config) - global.publish('test_table', { type: 'table' }) - - // Apply again (simulating multiple calls) - autoAssignActions(config) - - const action = global.dataform.actions[0] - const reservationCount = action.contextablePreOps.filter(op => - op.includes('SET @@reservation') - ).length - expect(reservationCount).toBe(1) - }) - - test('should handle mixed case DECLARE at outer level', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.mixed_case'] - } - ] - - autoAssignActions(config) - global.operate('mixed_case').queries(` - declare x INT64 DEFAULT 1; - SELECT x; - `) - - const action = global.dataform.actions[0] - expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) - - test('should not skip DECLARE inside BEGIN...END block', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.begin_declare'] - } - ] - - autoAssignActions(config) - global.operate('begin_declare').queries(` - --DECLARE x INT64 DEFAULT 1; - # comment - BEGIN - DECLARE x INT64 DEFAULT 1; - SELECT x; - END; - `) - - const action = global.dataform.actions[0] - expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) - - test('should not skip DECLARE inside EXECUTE IMMEDIATE', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.exec_declare'] - } - ] - - autoAssignActions(config) - global.operate('exec_declare').queries(` - /* - block comment - DECLARE x INT64; - */ - EXECUTE IMMEDIATE "DECLARE x INT64; SET x = 1; SELECT x;" - `) - - const action = global.dataform.actions[0] - expect(action.proto.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) - - test('should skip DECLARE after SQL comments', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.comment_declare'] - } - ] - - autoAssignActions(config) - global.operate('comment_declare').queries(` - -- set up variables - # comment - /* block comment */ - /* - multi-line block comment - */ - DECLARE x INT64 DEFAULT 1; - SELECT x; - `) - - const action = global.dataform.actions[0] - expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) - - test('should handle array of queries with outer DECLARE', () => { - const config = [ - { - tag: 'test', - reservation: 'projects/test/locations/US/reservations/prod', - actions: ['test-project.test-schema.array_queries'] - } - ] - - autoAssignActions(config) - global.operate('array_queries').queries([ - 'DECLARE x INT64 DEFAULT 1;', - 'SELECT x;' - ]) - - const action = global.dataform.actions[0] - expect(action.proto.queries).not.toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') - }) }) }) From 719db941aaf1958aa7ff54689b6e65c3f7671edb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:20:30 +0000 Subject: [PATCH 03/22] Bump eslint from 10.0.0 to 10.0.1 (#41) Bumps [eslint](https://github.com/eslint/eslint) from 10.0.0 to 10.0.1. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.0.0...v10.0.1) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.0.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 472 +++++++++++++++++++++++++++++++++++++++------- package.json | 2 +- 2 files changed, 409 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index a360960..bd9c393 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-only", "devDependencies": { "@eslint/js": "10.0.1", - "eslint": "10.0.0", + "eslint": "10.0.1", "globals": "17.3.0", "jest": "30.2.0" }, @@ -514,6 +514,40 @@ "dev": true, "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -557,31 +591,54 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz", - "integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.1", + "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", - "minimatch": "^10.1.1" + "minimatch": "^10.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -635,9 +692,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz", - "integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -710,29 +767,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1260,6 +1294,19 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1311,6 +1358,17 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1445,6 +1503,34 @@ "dev": true, "license": "ISC" }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", @@ -1459,10 +1545,237 @@ "darwin" ] }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2113,15 +2426,15 @@ } }, "node_modules/eslint": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz", - "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.1.tgz", + "integrity": "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.0", + "@eslint/config-array": "^0.23.2", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", @@ -2133,9 +2446,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.0", - "eslint-visitor-keys": "^5.0.0", - "espree": "^11.1.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2146,7 +2459,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.1.1", + "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2169,9 +2482,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", - "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2188,9 +2501,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2200,32 +2513,55 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/eslint/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/espree": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", - "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.0" + "eslint-visitor-keys": "^5.0.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -4507,6 +4843,14 @@ "node": ">=8.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 2cafb86..3118ab6 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "jest": "30.2.0", - "eslint": "10.0.0", + "eslint": "10.0.1", "@eslint/js": "10.0.1", "globals": "17.3.0" }, From 09840a4a95b54c3e090e9c24e19e85224843d862 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:20:31 +0000 Subject: [PATCH 04/22] Bump @dataform/core from 3.0.46 to 3.0.47 in /test-project (#42) Bumps [@dataform/core](https://github.com/dataform-co/dataform) from 3.0.46 to 3.0.47. - [Release notes](https://github.com/dataform-co/dataform/releases) - [Commits](https://github.com/dataform-co/dataform/compare/3.0.46...3.0.47) --- updated-dependencies: - dependency-name: "@dataform/core" dependency-version: 3.0.47 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test-project/package-lock.json | 8 ++++---- test-project/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test-project/package-lock.json b/test-project/package-lock.json index fd542a0..2d8d05f 100644 --- a/test-project/package-lock.json +++ b/test-project/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "test-dataform-project", "dependencies": { - "@dataform/core": "3.0.46", + "@dataform/core": "3.0.47", "@masthead-data/dataform-package": "file:../" }, "devDependencies": { @@ -4237,9 +4237,9 @@ } }, "node_modules/@dataform/core": { - "version": "3.0.46", - "resolved": "https://registry.npmjs.org/@dataform/core/-/core-3.0.46.tgz", - "integrity": "sha512-jpeeJIxhpZ1zSiwvntTpeUHRDDNUBu00rswaygxNCnVPZo26a34HayXocGz8j1lbriqX0WAeswNxkZ/XNZhByA==", + "version": "3.0.47", + "resolved": "https://registry.npmjs.org/@dataform/core/-/core-3.0.47.tgz", + "integrity": "sha512-cjShEjmOMOGBL5mlgn8lXwrUIrlSPWj6JP7CPcgHfZHBGs6+sDBJ9aqH5slJG+oFO1iI6kmJGo95+pbG64TcMw==", "license": "Apache-2.0" }, "node_modules/@google-cloud/bigquery": { diff --git a/test-project/package.json b/test-project/package.json index 43baba2..f00d3f5 100644 --- a/test-project/package.json +++ b/test-project/package.json @@ -2,7 +2,7 @@ "name": "test-dataform-project", "description": "Test Dataform project for dataform-package", "dependencies": { - "@dataform/core": "3.0.46", + "@dataform/core": "3.0.47", "@masthead-data/dataform-package": "file:../" }, "devDependencies": { From aa8a15a5c317dc65d5539f0aa8dd2a066d9610ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:13:20 +0000 Subject: [PATCH 05/22] Bump @dataform/cli from 3.0.46 to 3.0.47 in /test-project (#43) Bumps [@dataform/cli](https://github.com/dataform-co/dataform) from 3.0.46 to 3.0.47. - [Release notes](https://github.com/dataform-co/dataform/releases) - [Commits](https://github.com/dataform-co/dataform/compare/3.0.46...3.0.47) --- updated-dependencies: - dependency-name: "@dataform/cli" dependency-version: 3.0.47 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> --- test-project/package-lock.json | 8 ++++---- test-project/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test-project/package-lock.json b/test-project/package-lock.json index 2d8d05f..56346ef 100644 --- a/test-project/package-lock.json +++ b/test-project/package-lock.json @@ -10,7 +10,7 @@ "@masthead-data/dataform-package": "file:../" }, "devDependencies": { - "@dataform/cli": "3.0.46" + "@dataform/cli": "3.0.47" } }, "..": { @@ -4204,9 +4204,9 @@ } }, "node_modules/@dataform/cli": { - "version": "3.0.46", - "resolved": "https://registry.npmjs.org/@dataform/cli/-/cli-3.0.46.tgz", - "integrity": "sha512-zqGHPkA8c4LGMp+FajDVr7fLx/StCMKTqeNSLoJT1GB5PLUAw4Xo8u+Q1nl/JNfKGacD8i7pIF16ZaQvYSCmGQ==", + "version": "3.0.47", + "resolved": "https://registry.npmjs.org/@dataform/cli/-/cli-3.0.47.tgz", + "integrity": "sha512-qcXYF7/J9FxtekcDU0EWBh1rCKNyWsZMQw9MVJsKHbMV2yWmxjI5PEz+C2oqOH3QaJ9y/RNFL4rC/yHTE8OUsw==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/test-project/package.json b/test-project/package.json index f00d3f5..95c9ff9 100644 --- a/test-project/package.json +++ b/test-project/package.json @@ -6,6 +6,6 @@ "@masthead-data/dataform-package": "file:../" }, "devDependencies": { - "@dataform/cli": "3.0.46" + "@dataform/cli": "3.0.47" } } From 02ff9af465e6d42cfd65d6650e5f823dae89caad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 01:18:29 +0000 Subject: [PATCH 06/22] Bump eslint from 10.0.1 to 10.0.2 (#48) Bumps [eslint](https://github.com/eslint/eslint) from 10.0.1 to 10.0.2. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.0.1...v10.0.2) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.0.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd9c393..82db441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-only", "devDependencies": { "@eslint/js": "10.0.1", - "eslint": "10.0.1", + "eslint": "10.0.2", "globals": "17.3.0", "jest": "30.2.0" }, @@ -1796,9 +1796,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2426,9 +2426,9 @@ } }, "node_modules/eslint": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.1.tgz", - "integrity": "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", "dependencies": { @@ -2442,7 +2442,7 @@ "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", diff --git a/package.json b/package.json index 3118ab6..f503e86 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "jest": "30.2.0", - "eslint": "10.0.1", + "eslint": "10.0.2", "@eslint/js": "10.0.1", "globals": "17.3.0" }, From 909747eacdef8f9d592e689f6ba6a0fd8d4fde46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 01:20:29 +0000 Subject: [PATCH 07/22] Bump globals from 17.3.0 to 17.4.0 (#49) Bumps [globals](https://github.com/sindresorhus/globals) from 17.3.0 to 17.4.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v17.3.0...v17.4.0) --- updated-dependencies: - dependency-name: globals dependency-version: 17.4.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82db441..1dbc32b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@eslint/js": "10.0.1", "eslint": "10.0.2", - "globals": "17.3.0", + "globals": "17.4.0", "jest": "30.2.0" }, "engines": { @@ -2927,9 +2927,9 @@ } }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index f503e86..1f6107b 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "jest": "30.2.0", "eslint": "10.0.2", "@eslint/js": "10.0.1", - "globals": "17.3.0" + "globals": "17.4.0" }, "files": [ "index.js", From 75897fc4012d9dada07131c00a63a3fd2e9c1595 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:59:43 +0100 Subject: [PATCH 08/22] Bump the npm_and_yarn group across 2 directories with 1 update (#44) Bumps the npm_and_yarn group with 1 update in the / directory: [minimatch](https://github.com/isaacs/minimatch). Bumps the npm_and_yarn group with 1 update in the /test-project directory: [minimatch](https://github.com/isaacs/minimatch). Updates `minimatch` from 3.1.2 to 3.1.5 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) Updates `minimatch` from 9.0.5 to 9.0.9 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: minimatch dependency-version: 9.0.9 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 26 +++++++++++------------ test-project/package-lock.json | 38 ++++++++++++++-------------------- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1dbc32b..bd59fb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -629,9 +629,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2537,9 +2537,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2911,13 +2911,13 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4017,9 +4017,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { diff --git a/test-project/package-lock.json b/test-project/package-lock.json index 56346ef..c9816d2 100644 --- a/test-project/package-lock.json +++ b/test-project/package-lock.json @@ -19,7 +19,7 @@ "license": "AGPL-3.0-only", "devDependencies": { "@eslint/js": "10.0.1", - "eslint": "10.0.0", + "eslint": "10.0.1", "globals": "17.3.0", "jest": "30.2.0" }, @@ -2175,11 +2175,13 @@ } }, "../node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3165,7 +3167,9 @@ } }, "../node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4929,13 +4933,15 @@ } }, "node_modules/editorconfig": { - "version": "1.0.4", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", "dev": true, "license": "MIT", "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", - "minimatch": "9.0.1", + "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { @@ -4945,20 +4951,6 @@ "node": ">=14" } }, - "node_modules/editorconfig/node_modules/minimatch": { - "version": "9.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/emoji-regex": { "version": "9.2.2", "dev": true, @@ -5646,11 +5638,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" From 4a82b89292f770846d9a1cb813be881f596b7f14 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:31:31 +0100 Subject: [PATCH 09/22] Update Dataform version in CI matrix and changelog to 3.0.48 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 7 ++++++- scripts/test-matrix.sh | 18 +++++------------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6501d3a..832e74d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dataform-version: ['2.4.2', '3.0.43'] + dataform-version: ['2.4.2', '3.0.48'] steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f6f7c..b73977f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +## [0.2.1] - 2026-03-12 + +- **Updated test version for v3.0 from 3.0.43 to 3.0.48** - Updated the Dataform version used in matrix testing to ensure compatibility with the latest stable release. + + ## [0.2.0] - 2026-01-20 ### Added - **`autoAssignActions()` method** - Primary integration approach that automatically assigns actions to reservations to all Dataform actions globally without requiring manual code in each action file -- **Matrix testing infrastructure** - Automated testing across multiple Dataform versions (currently - v2.4.2 and v3.0.43) +- **Matrix testing** - Automated testing across multiple Dataform versions (currently - v2.4.2 and v3.0.43) - **API Reference section** in README with comprehensive documentation of all exported methods ## [0.1.0] - 2025-10-27 diff --git a/scripts/test-matrix.sh b/scripts/test-matrix.sh index 1f89b4f..4c26729 100755 --- a/scripts/test-matrix.sh +++ b/scripts/test-matrix.sh @@ -5,12 +5,9 @@ set -e if [ $# -gt 0 ]; then VERSIONS=("$@") else - VERSIONS=("2.4.2" "3.0.43" "local") + VERSIONS=("2.4.2" "3.0.48") fi -# Setup local path constant -DATAFORM_LOCAL_PATH="${DATAFORM_LOCAL_PATH:-/Users/maxostapenko/GitHub/dataform}" - # Cleanup function to restore configuration files cleanup() { if [ -f "dataform.json.bak" ]; then @@ -39,7 +36,7 @@ for VERSION in "${VERSIONS[@]}"; do echo "=========================================" # Configuration management based on version - if [[ $VERSION == 3* || $VERSION == "local" ]]; then + if [[ $VERSION == 3* ]]; then if [ -f "dataform.json" ]; then echo "Hiding dataform.json for v3 compatibility" mv dataform.json dataform.json.bak @@ -55,14 +52,9 @@ for VERSION in "${VERSIONS[@]}"; do # echo "Cleaning up previous @dataform installations..." # npm uninstall @dataform/cli @dataform/core --no-save > /dev/null 2>&1 || true - if [[ "$VERSION" == "local" ]]; then - echo "Using local Dataform dependency from $DATAFORM_LOCAL_PATH..." - npm install "$DATAFORM_LOCAL_PATH/bazel-bin/packages/@dataform/cli/package" "$DATAFORM_LOCAL_PATH/bazel-bin/packages/@dataform/core/package" --no-save - else - echo "Installing Dataform @$VERSION..." - # Use --no-save to avoid cluttering package.json/package-lock.json during matrix tests - npm install @dataform/cli@$VERSION @dataform/core@$VERSION --no-save - fi + echo "Installing Dataform @$VERSION..." + # Use --no-save to avoid cluttering package.json/package-lock.json during matrix tests + npm install @dataform/cli@$VERSION @dataform/core@$VERSION --no-save # Run tests using the single version command equivalent but passing the version npx @dataform/cli compile --json --vars=DATAFORM_VERSION=$VERSION > compiled.json From 533850d07a874d76d3689e0696a9db9fb3e9bb79 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:32:09 +0100 Subject: [PATCH 10/22] Refactor isNativeReservationSupported to always return false --- index.js | 49 ++----------------------------------------------- 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/index.js b/index.js index 17d405f..bbccd7e 100644 --- a/index.js +++ b/index.js @@ -131,53 +131,8 @@ function isNativeReservationSupported() { return hasNativeReservationSupportCache } - if (global.dataform && global.dataform.projectConfig) { - const version = global.dataform.projectConfig.dataformCoreVersion - let actualVersion = version - - if (!actualVersion) { - try { - const path = require('path') - const fs = require('fs') - - // When running tests, the dataform framework is actually in test-project/node_modules - // require() relative to __dirname will load the parent project's devDependencies, not the exact version we are testing. - const localPkg = path.resolve(process.cwd(), 'node_modules/@dataform/core/package.json') - if (fs.existsSync(localPkg)) { - const content = fs.readFileSync(localPkg, 'utf8') - const corePkg = JSON.parse(content) - if (corePkg && corePkg.version) { - actualVersion = corePkg.version - } - } - } catch { - // Ignore if package.json cannot be read - } - } - - // Fallback: Check for DATAFORM_VERSION in projectConfig.vars - if (!actualVersion && global.dataform.projectConfig.vars && global.dataform.projectConfig.vars.DATAFORM_VERSION) { - actualVersion = global.dataform.projectConfig.vars.DATAFORM_VERSION - } - - if (actualVersion) { - // As a simple heuristic for backwards compatibility testing: anything matching 2.* or 3.0.* (up to 43) gets false. - if (actualVersion.startsWith('2.') || actualVersion === '3.0.43') { - hasNativeReservationSupportCache = false - return false - } - } else { - // If version is still unknown, check for v3 specific properties - // v3 session has some different properties, but projectConfig exists in both. - // If we can't detect version, and it's not v3.0.44+, we might want to default to false - // for safety, but typically users of this package will be on v3. - // However, for 2.x, we definitely want false. - // If we are here, we couldn't find the version. - } - } - - hasNativeReservationSupportCache = true - return true + hasNativeReservationSupportCache = false + return false } /** From 50f32358250b7dc334469ab553dc24324bb06ac9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:20:30 +0000 Subject: [PATCH 11/22] Bump eslint from 10.0.0 to 10.0.1 (#41) Bumps [eslint](https://github.com/eslint/eslint) from 10.0.0 to 10.0.1. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.0.0...v10.0.1) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.0.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f6107b..fb8c00a 100644 --- a/package.json +++ b/package.json @@ -50,4 +50,4 @@ "registry": "https://registry.npmjs.org/", "access": "public" } -} \ No newline at end of file +} From 3d3e7caa5192463c9da925448ba5e50eba927be9 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:50:28 +0100 Subject: [PATCH 12/22] =?UTF-8?q?=F0=9F=A7=B9=20Refactor=20duplicated=20lo?= =?UTF-8?q?gic=20in=20applyReservationToAction=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: deduplicate logic in applyReservationToAction - Extracted duplicated logic for prepending statements into `prependStatement` and `isArrayOrString` helpers. - Refactored `applyReservationToAction` to use these helpers for `contextablePreOps`, `contextableQueries`, `proto.preOps`, and `proto.queries`. - Simplified the `action.queries` monkeypatch using `prependStatement`. - Ensured non-mutating behavior for array prepending. - Restored `test-project/workflow_settings.yaml` that was accidentally renamed during matrix tests. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * refactor: update exports and enhance test coverage with new utility functions --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- index.js | 168 +++++++++++++++++++----------------------- test/index.test.js | 178 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 228 insertions(+), 118 deletions(-) diff --git a/index.js b/index.js index bbccd7e..d5c223d 100644 --- a/index.js +++ b/index.js @@ -109,7 +109,6 @@ function createReservationSetter(config) { if (!actionName) return '' const reservation = findReservation(actionName, configSets) - if (!reservation) return '' return reservation ? `SET @@reservation='${reservation}';` @@ -180,6 +179,35 @@ function hasOuterDeclare(sql) { return /^DECLARE\b/i.test(s) } +/** + * Ensures a statement is prepended to an array or string + * @param {Array|string} target - The target to prepend to + * @param {string} statement - The statement to prepend + * @returns {Array|string} The modified target + */ +function prependStatement(target, statement) { + if (Array.isArray(target)) { + if (!target.includes(statement)) { + return [statement, ...target] + } + return target + } + if (typeof target === 'string') { + if (!target.includes(statement)) { + return [statement, target] + } + } + return target +} + +/** + * Checks if a value is an array or string + * @param {any} val - The value to check + * @returns {boolean} True if array or string + */ +function isArrayOrString(val) { + return Array.isArray(val) || typeof val === 'string' +} /** * Helper to apply reservation to a single action * @param {Object} action - Dataform action object @@ -223,112 +251,65 @@ function applyReservationToAction(action, configSets) { // Old Approach (SQL Prepending) const statement = `SET @@reservation='${reservation}';` - // For operation builders, the queries are often set AFTER the builder is created via .queries() - // We monkeypatch the .queries() method to ensure our statement is always prepended. - if (isOperation && typeof action.queries === 'function' && !action._queriesPatched) { - const originalQueriesFn = action.queries - action.queries = function (queries) { - let queriesArray = queries - - // Check for outer DECLARE before wrapping - if (hasOuterDeclare(queries)) { - return originalQueriesFn.apply(this, [queries]) - } - - if (typeof queries === 'function') { - queriesArray = (ctx) => { - const result = queries(ctx) - if (typeof result === 'string') { - return [statement, result] - } else if (Array.isArray(result)) { - return [statement, ...result] - } - return result - } - } else if (typeof queries === 'string') { - queriesArray = [statement, queries] - } else if (Array.isArray(queries)) { - // Check if already prepended to avoid duplicates - if (!queries.includes(statement)) { - queriesArray = [statement, ...queries] - } - } - return originalQueriesFn.apply(this, [queriesArray]) + // For operation builders, the queries are often set AFTER the builder is created via .queries() + // We monkeypatch the .queries() method to ensure our statement is always prepended. + if (isOperation && typeof action.queries === 'function' && !action._queriesPatched) { + const originalQueriesFn = action.queries + action.queries = function (queries) { + // Check for outer DECLARE before wrapping + if (hasOuterDeclare(queries)) { + return originalQueriesFn.apply(this, [queries]) } - action._queriesPatched = true + + const queriesArray = typeof queries === 'function' + ? (ctx) => prependStatement(queries(ctx), statement) + : prependStatement(queries, statement) + return originalQueriesFn.apply(this, [queriesArray]) } + action._queriesPatched = true + } // Prefer modifying data structure directly if we know it's a safe type // This handles both Builders (via .proto) and Compiled Objects (direct) - // 1. Try contextablePreOps (Tables/Views Builders before resolution) - if (action.contextablePreOps) { - if (!hasOuterDeclare(action.contextablePreOps)) { - if (Array.isArray(action.contextablePreOps)) { - if (!action.contextablePreOps.includes(statement)) { - action.contextablePreOps.unshift(statement) - } - } else if (typeof action.contextablePreOps === 'string') { - if (!action.contextablePreOps.includes(statement)) { - action.contextablePreOps = [statement, action.contextablePreOps] - } - } - } + // 1. Try contextablePreOps (Tables/Views Builders before resolution) + if (action.contextablePreOps) { + if (!hasOuterDeclare(action.contextablePreOps)) { + action.contextablePreOps = prependStatement(action.contextablePreOps, statement) } - // 2. Try contextableQueries (Operations Builders before resolution) - else if (action.contextableQueries) { + } + // 2. Try contextableQueries (Operations Builders before resolution) + else if (action.contextableQueries) { // Skip if there is an outer DECLARE if (!hasOuterDeclare(action.contextableQueries)) { - if (Array.isArray(action.contextableQueries)) { - if (!action.contextableQueries.includes(statement)) { - action.contextableQueries.unshift(statement) - } - } else if (typeof action.contextableQueries === 'string') { - if (!action.contextableQueries.includes(statement)) { - action.contextableQueries = [statement, action.contextableQueries] - } - } + action.contextableQueries = prependStatement(action.contextableQueries, statement) } - } - // 3. Try proto.preOps (Compiled Tables/Views or Resolved Builders) - else if (hasType) { - if (!hasOuterDeclare(proto.preOps || [])) { - if (!proto.preOps) { - proto.preOps = [] - } - if (Array.isArray(proto.preOps)) { - if (!proto.preOps.includes(statement)) { - proto.preOps.unshift(statement) - } - } else if (typeof proto.preOps === 'string') { - if (!proto.preOps.includes(statement)) { - proto.preOps = [statement, proto.preOps] - } - } else if (hasPreOpsFn) { - action.preOps(statement) - } + } + // 3. Try proto.preOps (Compiled Tables/Views or Resolved Builders) + else if (hasType) { + if (!hasOuterDeclare(proto.preOps || [])) { + if (!proto.preOps) { + proto.preOps = [] } - } - // 4. Try proto.queries (Compiled Operations or Resolved Builders) - else if (proto.queries) { - // Skip if there is an outer DECLARE - if (!hasOuterDeclare(proto.queries)) { - if (Array.isArray(proto.queries)) { - if (!proto.queries.includes(statement)) { - proto.queries.unshift(statement) - } - } else if (typeof proto.queries === 'string') { - if (!proto.queries.includes(statement)) { - proto.queries = [statement, proto.queries] - } - } + + if (isArrayOrString(proto.preOps)) { + proto.preOps = prependStatement(proto.preOps, statement) + } else if (hasPreOpsFn) { + action.preOps(statement) } } - // 5. Fallback to function API (likely Tables/Views) - else if (hasPreOpsFn) { - action.preOps(statement) + } + // 4. Try proto.queries (Compiled Operations or Resolved Builders) + else if (proto.queries) { + // Skip if there is an outer DECLARE + if (!hasOuterDeclare(proto.queries)) { + proto.queries = prependStatement(proto.queries, statement) } } + // 5. Fallback to function API (likely Tables/Views) + else if (hasPreOpsFn) { + action.preOps(statement) + } } } @@ -385,5 +366,8 @@ module.exports = { createReservationSetter, getActionName, autoAssignActions, + prependStatement, + isArrayOrString, + findReservation, isNativeReservationSupported } diff --git a/test/index.test.js b/test/index.test.js index 37053d2..9be4c75 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -5,30 +5,48 @@ const { isNativeReservationSupported } = require('../index') -// Example configuration for testing (previously hardcoded in the package) +/** + * TEST NAMING GUIDELINE + * + * Use descriptive semantic names highlighting environment and purpose: + * - Environment prefix: prod, dev, staging (reflects the business context) + * - Dataset: public_analytics, data_mart, assertions, staging (describes domain) + * - Table/action: pages, requests, quality_check (specific dataset contents) + * + * Format: {environment}.{dataset}.{table} + * Example: prod.public_analytics.pages, prod.data_mart.requests_latest + * + * Benefits: Tests are self-documenting, easier to extend, and reflect real-world patterns. + */ + +// Example configuration for testing +// Naming guideline: Use descriptive semantic names reflecting environment/purpose +// - Environment prefix: prod, dev, staging +// - Dataset describes the business domain: public_analytics, data_mart, assertions, staging +// - Table/action names describe the dataset contents: pages, requests, quality_check, temp_dataset const EXAMPLE_RESERVATION_CONFIG = [ { - tag: 'high_slots', - reservation: 'projects/httparchive/locations/US/reservations/pipeline', + tag: 'prod_reserved', + reservation: 'projects/my-project/locations/us/reservations/prod', actions: [ - 'httparchive.crawl.pages', - 'httparchive.crawl.requests', - 'httparchive.crawl.parsed_css', - 'httparchive.f1.pages_latest', - 'httparchive.f1.requests_latest' + 'prod.public_analytics.pages', + 'prod.public_analytics.requests', + 'prod.public_analytics.parsed_css', + 'prod.data_mart.pages_latest', + 'prod.data_mart.requests_latest' ] }, { - tag: 'low_slots', + tag: 'dev_reserved', reservation: null, actions: [] }, { - tag: 'on_demand', + tag: 'ondemand_pricing', reservation: 'none', actions: [ - 'httparchive.dataform_assertions.corrupted_technology_values', - 'httparchive.scratchspace.new' + 'prod.assertions.data_quality_check', + 'prod.staging.temp_dataset' ] } ] @@ -51,22 +69,22 @@ describe('Dataform package', () => { test('should return empty string when native reservation is supported', () => { process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'true' - const ctx = { self: () => 'httparchive.crawl.pages' } + const ctx = { self: () => 'prod.staging.temp_dataset' } const result = reservation_setter(ctx) expect(result).toBe('') process.env.DATAFORM_MOCK_NATIVE_RESERVATION = 'false' }) - test('should return reservation SQL for high_slots action', () => { + test('should return reservation SQL for prod_reserved action', () => { const ctx = { - self: () => 'httparchive.crawl.pages' + self: () => 'prod.public_analytics.pages' } const result = reservation_setter(ctx) - expect(result).toBe('SET @@reservation=\'projects/httparchive/locations/US/reservations/pipeline\';') + expect(result).toBe('SET @@reservation=\'projects/my-project/locations/us/reservations/prod\';') }) - test('should return empty string for low_slots action', () => { + test('should return empty string for dev_reserved action', () => { const ctx = { self: () => 'some.unknown.action' } @@ -75,9 +93,9 @@ describe('Dataform package', () => { expect(result).toBe('') }) - test('should return none reservation for on_demand action', () => { + test('should return none reservation for ondemand_pricing action', () => { const ctx = { - self: () => 'httparchive.dataform_assertions.corrupted_technology_values' + self: () => 'prod.assertions.data_quality_check' } const result = reservation_setter(ctx) @@ -89,8 +107,8 @@ describe('Dataform package', () => { operation: { proto: { target: { - database: 'httparchive', - schema: 'crawl', + database: 'prod', + schema: 'public_analytics', name: 'requests' } } @@ -98,7 +116,7 @@ describe('Dataform package', () => { } const result = reservation_setter(ctx) - expect(result).toBe('SET @@reservation=\'projects/httparchive/locations/US/reservations/pipeline\';') + expect(result).toBe('SET @@reservation=\'projects/my-project/locations/us/reservations/prod\';') }) test('should throw error when invalid config is provided to createReservationSetter', () => { @@ -145,11 +163,11 @@ describe('Dataform package', () => { test('should strip backticks from action name', () => { const ctx = { - self: () => '`httparchive.crawl.pages`' + self: () => '`prod.public_analytics.pages`' } const result = reservation_setter(ctx) - expect(result).toBe('SET @@reservation=\'projects/httparchive/locations/US/reservations/pipeline\';') + expect(result).toBe('SET @@reservation=\'projects/my-project/locations/us/reservations/prod\';') }) test('should handle malformed proto target', () => { @@ -228,7 +246,7 @@ describe('Dataform package', () => { // We use a separate describe or just accept cache in main tests. // Since we can't easily clear the internal cache without jest.resetModules(), - // this test aims to verify the logic if it hasn't cached yet, + // this test aims to verify the logic if it hasn't cached yet, // or just ensure it doesn't crash. global.dataform = { projectConfig: { @@ -237,7 +255,7 @@ describe('Dataform package', () => { } const result = isNativeReservationSupported() - // If cached as true by previous tests, this might be true. + // If cached as true by previous tests, this might be true. // But we can at least verify it's a boolean. expect(typeof result).toBe('boolean') }) @@ -747,4 +765,112 @@ describe('Dataform package', () => { expect(() => autoAssignActions([])).toThrow('Configuration array cannot be empty') }) }) + + describe('prependStatement', () => { + test('should prepend statement to array', () => { + const { prependStatement } = require('../index') + const result = prependStatement(['query2'], 'query1') + expect(result).toEqual(['query1', 'query2']) + }) + + test('should not duplicate statement in array', () => { + const { prependStatement } = require('../index') + const result = prependStatement(['query1', 'query2'], 'query1') + expect(result).toEqual(['query1', 'query2']) + }) + + test('should prepend statement to string', () => { + const { prependStatement } = require('../index') + const result = prependStatement('SELECT * FROM table', 'SET @var=1;') + expect(result).toEqual(['SET @var=1;', 'SELECT * FROM table']) + }) + + test('should not duplicate statement in string', () => { + const { prependStatement } = require('../index') + const duplicate = 'SET @var=1;\nSELECT * FROM table' + const result = prependStatement(duplicate, 'SET @var=1;') + expect(result).toBe(duplicate) + }) + + test('should handle empty array', () => { + const { prependStatement } = require('../index') + const result = prependStatement([], 'statement') + expect(result).toEqual(['statement']) + }) + + test('should handle empty string', () => { + const { prependStatement } = require('../index') + const result = prependStatement('', 'statement') + expect(result).toEqual(['statement', '']) + }) + }) + + describe('isArrayOrString', () => { + test('should return true for array', () => { + const { isArrayOrString } = require('../index') + expect(isArrayOrString([])).toBe(true) + expect(isArrayOrString(['item'])).toBe(true) + }) + + test('should return true for string', () => { + const { isArrayOrString } = require('../index') + expect(isArrayOrString('')).toBe(true) + expect(isArrayOrString('test')).toBe(true) + }) + + test('should return false for other types', () => { + const { isArrayOrString } = require('../index') + expect(isArrayOrString(null)).toBe(false) + expect(isArrayOrString(undefined)).toBe(false) + expect(isArrayOrString(123)).toBe(false) + expect(isArrayOrString({})).toBe(false) + expect(isArrayOrString(() => {})).toBe(false) + }) + }) + + describe('findReservation', () => { + test('should find reservation for matching action', () => { + const { findReservation } = require('../index') + const configSets = [{ + actionSet: new Set(['prod.dataset.table']), + reservation: 'projects/test/reservations/prod' + }] + const result = findReservation('prod.dataset.table', configSets) + expect(result).toBe('projects/test/reservations/prod') + }) + + test('should return null for non-matching action', () => { + const { findReservation } = require('../index') + const configSets = [{ + actionSet: new Set(['prod.dataset.table']), + reservation: 'projects/test/reservations/prod' + }] + const result = findReservation('unknown.dataset.table', configSets) + expect(result).toBeNull() + }) + + test('should handle multiple config sets', () => { + const { findReservation } = require('../index') + const configSets = [ + { actionSet: new Set(['prod.dataset.table']), reservation: 'prod-res' }, + { actionSet: new Set(['dev.dataset.table']), reservation: 'dev-res' } + ] + expect(findReservation('prod.dataset.table', configSets)).toBe('prod-res') + expect(findReservation('dev.dataset.table', configSets)).toBe('dev-res') + }) + + test('should return null for null/undefined action name', () => { + const { findReservation } = require('../index') + const configSets = [{ actionSet: new Set(['test']), reservation: 'res' }] + expect(findReservation(null, configSets)).toBeNull() + expect(findReservation(undefined, configSets)).toBeNull() + }) + + test('should return null for non-string action name', () => { + const { findReservation } = require('../index') + const configSets = [{ actionSet: new Set(['test']), reservation: 'res' }] + expect(findReservation(123, configSets)).toBeNull() + expect(findReservation({}, configSets)).toBeNull() + }) + }) }) From 811cb5f5284588ad209d7cc65480c2cdd1bc8458 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:30:10 +0100 Subject: [PATCH 13/22] =?UTF-8?q?=E2=9A=A1=20Optimize=20reservation=20look?= =?UTF-8?q?up=20using=20Map=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⚡ Optimize reservation lookup using Map - Refactored `preprocessConfig` to build an optimized `Map` for reservation lookups. - Updated `findReservation` to perform O(1) lookups using the `Map`. - Preserved "first match wins" logic and existing validation. - Maintained backward compatibility by preserving original data structures in preprocessed config. - Improved lookup performance by ~24x in benchmarks. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * Refactor findReservation tests to use Map for action-reservation mapping --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- index.js | 47 +++++++++++++++++++++++++++------------------- test/index.test.js | 42 ++++++++++++++++++++--------------------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/index.js b/index.js index d5c223d..c2eae3a 100644 --- a/index.js +++ b/index.js @@ -24,27 +24,21 @@ function getActionName(ctx) { /** * Finds the matching reservation for an action name * @param {string} actionName - The action name to look up - * @param {Array} configSets - Preprocessed configuration with Sets + * @param {Map} actionToReservation - Preprocessed configuration Map (actionName -> reservation) * @returns {string|null} The reservation identifier or null */ -function findReservation(actionName, configSets) { +function findReservation(actionName, actionToReservation) { if (!actionName || typeof actionName !== 'string') { return null } - for (const config of configSets) { - if (config.actionSet.has(actionName)) { - return config.reservation - } - } - - return null + return actionToReservation.get(actionName) ?? null } /** * Validates and preprocesses the configuration array * @param {Array} config - Raw configuration array - * @returns {Array} Preprocessed configuration with Sets + * @returns {Object} Preprocessed configuration containing both Map and original structure */ function preprocessConfig(config) { if (!config || !Array.isArray(config)) { @@ -55,7 +49,8 @@ function preprocessConfig(config) { throw new Error('Configuration array cannot be empty') } - return config.map((item, index) => { + const actionToReservation = new Map() + const configSets = config.map((item, index) => { if (!item || typeof item !== 'object') { throw new Error(`Configuration item at index ${index} must be an object`) } @@ -68,13 +63,27 @@ function preprocessConfig(config) { throw new Error(`Configuration item at index ${index} must have 'actions' as an array`) } - return { + const configItem = { tag: item.tag, reservation: item.reservation, actions: item.actions, actionSet: new Set(item.actions) } + + // Populate Map while preserving "first match wins" behavior + configItem.actions.forEach(actionName => { + if (!actionToReservation.has(actionName)) { + actionToReservation.set(actionName, item.reservation) + } + }) + + return configItem }) + + return { + actionToReservation, + configSets + } } /** @@ -211,9 +220,9 @@ function isArrayOrString(val) { /** * Helper to apply reservation to a single action * @param {Object} action - Dataform action object - * @param {Array} configSets - Preprocessed configuration + * @param {Map} actionToReservation - Preprocessed configuration Map */ -function applyReservationToAction(action, configSets) { +function applyReservationToAction(action, actionToReservation) { // 1. Identify where the data lives // If no .proto, assume action itself is the data container (compiled object) const proto = action.proto || action @@ -239,7 +248,7 @@ function applyReservationToAction(action, configSets) { } // 3. Apply Reservation - const reservation = findReservation(actionName, configSets) + const reservation = findReservation(actionName, actionToReservation) if (reservation) { if (isNativeReservationSupported()) { // New Approach (Native) @@ -318,12 +327,12 @@ function applyReservationToAction(action, configSets) { * @param {Array} config - Array of reservation configuration objects */ function autoAssignActions(config) { - const preprocessedConfig = preprocessConfig(config) + const { actionToReservation } = preprocessConfig(config) // 1. Process existing actions (in case this is called late) if (global.dataform && global.dataform.actions) { global.dataform.actions.forEach(action => { - applyReservationToAction(action, preprocessedConfig) + applyReservationToAction(action, actionToReservation) }) } @@ -339,7 +348,7 @@ function autoAssignActions(config) { // The action should be the last one added to the session if (global.dataform && global.dataform.actions && global.dataform.actions.length > 0) { const lastAction = global.dataform.actions[global.dataform.actions.length - 1] - applyReservationToAction(lastAction, preprocessedConfig) + applyReservationToAction(lastAction, actionToReservation) } return actionBuilder @@ -355,7 +364,7 @@ function autoAssignActions(config) { if (global.dataform && global.dataform.actions && global.dataform.actions.length > 0) { const lastAction = global.dataform.actions[global.dataform.actions.length - 1] - applyReservationToAction(lastAction, preprocessedConfig) + applyReservationToAction(lastAction, actionToReservation) } return result } diff --git a/test/index.test.js b/test/index.test.js index 9be4c75..aa1f42e 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -831,46 +831,44 @@ describe('Dataform package', () => { describe('findReservation', () => { test('should find reservation for matching action', () => { const { findReservation } = require('../index') - const configSets = [{ - actionSet: new Set(['prod.dataset.table']), - reservation: 'projects/test/reservations/prod' - }] - const result = findReservation('prod.dataset.table', configSets) + const actionToReservation = new Map([ + ['prod.dataset.table', 'projects/test/reservations/prod'] + ]) + const result = findReservation('prod.dataset.table', actionToReservation) expect(result).toBe('projects/test/reservations/prod') }) test('should return null for non-matching action', () => { const { findReservation } = require('../index') - const configSets = [{ - actionSet: new Set(['prod.dataset.table']), - reservation: 'projects/test/reservations/prod' - }] - const result = findReservation('unknown.dataset.table', configSets) + const actionToReservation = new Map([ + ['prod.dataset.table', 'projects/test/reservations/prod'] + ]) + const result = findReservation('unknown.dataset.table', actionToReservation) expect(result).toBeNull() }) test('should handle multiple config sets', () => { const { findReservation } = require('../index') - const configSets = [ - { actionSet: new Set(['prod.dataset.table']), reservation: 'prod-res' }, - { actionSet: new Set(['dev.dataset.table']), reservation: 'dev-res' } - ] - expect(findReservation('prod.dataset.table', configSets)).toBe('prod-res') - expect(findReservation('dev.dataset.table', configSets)).toBe('dev-res') + const actionToReservation = new Map([ + ['prod.dataset.table', 'prod-res'], + ['dev.dataset.table', 'dev-res'] + ]) + expect(findReservation('prod.dataset.table', actionToReservation)).toBe('prod-res') + expect(findReservation('dev.dataset.table', actionToReservation)).toBe('dev-res') }) test('should return null for null/undefined action name', () => { const { findReservation } = require('../index') - const configSets = [{ actionSet: new Set(['test']), reservation: 'res' }] - expect(findReservation(null, configSets)).toBeNull() - expect(findReservation(undefined, configSets)).toBeNull() + const actionToReservation = new Map([['test', 'res']]) + expect(findReservation(null, actionToReservation)).toBeNull() + expect(findReservation(undefined, actionToReservation)).toBeNull() }) test('should return null for non-string action name', () => { const { findReservation } = require('../index') - const configSets = [{ actionSet: new Set(['test']), reservation: 'res' }] - expect(findReservation(123, configSets)).toBeNull() - expect(findReservation({}, configSets)).toBeNull() + const actionToReservation = new Map([['test', 'res']]) + expect(findReservation(123, actionToReservation)).toBeNull() + expect(findReservation({}, actionToReservation)).toBeNull() }) }) }) From 7f96fb500229736dac9757a898d288ea838c044c Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:33:04 +0100 Subject: [PATCH 14/22] fix badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6485b4..971e43f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Masthead Package for Dataform -![NPM Version](https://img.shields.io/npm/v/%40masthead-data%2Fdataform-package) +[![NPM Version](https://img.shields.io/npm/v/%40masthead-data%2Fdataform-package)](https://www.npmjs.com/package/@masthead-data/dataform-package) ## Overview From 804e997cec21f1a92ae2a2ccf3b90445bfe1e188 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:52:51 +0100 Subject: [PATCH 15/22] Update changelog for version 0.2.1 --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b73977f..b7587bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security -## [0.2.1] - 2026-03-12 +## [0.2.1] - 2026-03-20 +### Changed + +- **SQL statement prepending logic refactored** +- **Reservation lookup performance improved** - Refactored to use a Set for O(1) lookups instead of array iteration, significantly improving performance for large reservation lists - **Updated test version for v3.0 from 3.0.43 to 3.0.48** - Updated the Dataform version used in matrix testing to ensure compatibility with the latest stable release. From eac4e2dca64570b2cba041bdefffdc9982370bde Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:14:09 +0100 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=A7=AA=20[testing=20improvement]=20?= =?UTF-8?q?Missing=20test=20for=20compiled=20objects=20(proto.preOps)=20(#?= =?UTF-8?q?46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🧪 test: add coverage for compiled objects and proto.preOps - Added comprehensive tests for compiled objects in `test/compiled_objects.test.js`. - Covered `proto.preOps` and `proto.queries` handling for both array and string types. - Verified fallback mechanisms and monkeypatching logic. - Increased line coverage of `index.js` to 100%. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * fix: standardize string quotes in compiled objects tests * refactor: simplify monkeypatched queries tests with parameterized cases --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- test/compiled_objects.test.js | 272 ++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 test/compiled_objects.test.js diff --git a/test/compiled_objects.test.js b/test/compiled_objects.test.js new file mode 100644 index 0000000..d702efa --- /dev/null +++ b/test/compiled_objects.test.js @@ -0,0 +1,272 @@ +const { autoAssignActions } = require('../index') + +describe('Compiled Objects and Edge Cases', () => { + let originalDataform + + beforeEach(() => { + // Save original global state + originalDataform = global.dataform + + // Reset global state + global.dataform = { + actions: [], + projectConfig: { + defaultDatabase: 'test-project', + defaultSchema: 'test-schema' + } + } + }) + + afterEach(() => { + // Restore original global state + global.dataform = originalDataform + }) + + const config = [ + { + tag: 'test', + reservation: 'projects/test/locations/US/reservations/prod', + actions: ['test-project.test-schema.target_action'] + } + ] + + test('should apply reservation to compiled object via proto.preOps (array)', () => { + const action = { + type: 'table', + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + }, + preOps: [] // This will be treated as proto.preOps because action.proto is undefined + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(action.preOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should initialize proto.preOps if it does not exist', () => { + const action = { + type: 'table', + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + } + // preOps is missing + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(action.preOps).toBeDefined() + expect(Array.isArray(action.preOps)).toBe(true) + expect(action.preOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should handle proto.preOps as a string', () => { + const action = { + type: 'table', + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + }, + preOps: 'SELECT 1;' + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(Array.isArray(action.preOps)).toBe(true) + expect(action.preOps[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(action.preOps[1]).toBe('SELECT 1;') + }) + + test('should apply reservation to compiled operation via proto.queries (array)', () => { + const action = { + queries: ['SELECT * FROM table'], + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + } + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(action.queries).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(action.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should handle proto.queries as a string', () => { + const action = { + queries: 'SELECT * FROM table', + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + } + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(Array.isArray(action.queries)).toBe(true) + expect(action.queries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(action.queries[1]).toBe('SELECT * FROM table') + }) + + test('should fallback to action.preOps() function if hasType is true but proto.preOps is not an array/string', () => { + const preOpsMock = jest.fn() + const action = { + type: 'table', + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + }, + preOps: preOpsMock // This is a function + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(preOpsMock).toHaveBeenCalledWith('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should fallback to action.preOps() function if no other method matches (Fallback 5)', () => { + const preOpsMock = jest.fn() + const action = { + // No type, no queries, no contextable fields + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + }, + preOps: preOpsMock + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(preOpsMock).toHaveBeenCalledWith('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should handle contextableQueries as a string', () => { + const action = { + contextableQueries: 'SELECT 1', + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + } + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(Array.isArray(action.contextableQueries)).toBe(true) + expect(action.contextableQueries[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test('should handle contextablePreOps as a string', () => { + const action = { + type: 'table', + contextablePreOps: 'SELECT 1', + target: { + database: 'test-project', + schema: 'test-schema', + name: 'target_action' + } + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + expect(Array.isArray(action.contextablePreOps)).toBe(true) + expect(action.contextablePreOps[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) + + test.each([ + { + name: 'function returning string', + input: () => 'SELECT 1', + validator: (result) => { + expect(result).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(result).toContain('SELECT 1') + } + }, + { + name: 'function returning array', + input: () => ['SELECT 1', 'SELECT 2'], + validator: (result) => { + expect(result[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + expect(result).toHaveLength(3) + } + }, + { + name: 'function returning other types', + input: () => null, + validator: (result) => { + expect(result).toBe(null) + } + }, + { + name: 'array', + input: ['SELECT 1'], + validator: (result) => { + expect(result[0]).toBe('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + } + } + ])('should handle monkeypatched queries with $name', ({ input, validator }) => { + const action = { + queries: function(q) { this.resolvedQueries = q; return this }, + proto: { + queries: [], + target: { database: 'test-project', schema: 'test-schema', name: 'target_action' } + } + } + global.dataform.actions.push(action) + + autoAssignActions(config) + + action.queries(input) + + // For arrays, `action.queries()` directly assigns resolvedQueries if not wrapped, + // but the monkeypatch will wrap them. We extract the resolved value for validation. + // If input is an array, it's wrapped and immediately modifies `resolvedQueries` via the original function. + // Our action mock just sets `this.resolvedQueries = q`. + // So if the patched function returns, it calls the original function. + // Let's resolve what to test: + if (typeof input === 'function') { + const wrappedFn = action.resolvedQueries + expect(typeof wrappedFn).toBe('function') + const result = wrappedFn({}) + validator(result) + } else { + validator(action.resolvedQueries) + } + }) + + test('should intercept sqlxAction', () => { + const sqlxActionMock = jest.fn(() => { + global.dataform.actions.push({ + type: 'table', + target: { database: 'test-project', schema: 'test-schema', name: 'target_action' }, + preOps: [] + }) + }) + global.dataform.sqlxAction = sqlxActionMock + + autoAssignActions(config) + + global.dataform.sqlxAction() + + const action = global.dataform.actions[0] + expect(action.preOps).toContain('SET @@reservation=\'projects/test/locations/US/reservations/prod\';') + }) +}) From 40caf9c25ffa07f202f6e6bd30a01265270407d6 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:03:01 +0100 Subject: [PATCH 17/22] Remove local integration testing instructions from CONTRIBUTING.md and update isNativeSupported logic in verify_compilation.js --- CONTRIBUTING.md | 9 --------- scripts/verify_compilation.js | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56ae0f3..aa05c34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,15 +48,6 @@ We welcome contributions to the Dataform package! This document provides guideli npm run lint ``` -### Local Integration Testing -The `test-project` is configured to use the local version of the package. In `test-project/package.json`: -```json -"dependencies": { - "@masthead-data/dataform-package": "file:../" -} -``` -**Note:** `npm ci` or `npm install` in the `test-project` caches the local package. If you make changes to `index.js` and don't see them reflected, you may need to force an update or avoid `npm ci` during rapid iteration. - ## Project Structure ```filetree diff --git a/scripts/verify_compilation.js b/scripts/verify_compilation.js index b5ecd4b..57cae8b 100644 --- a/scripts/verify_compilation.js +++ b/scripts/verify_compilation.js @@ -12,7 +12,7 @@ function verify() { const version = process.argv[2] console.log(`Running verification for Dataform version: ${version}`) - const isNativeSupported = version === 'local' + const isNativeSupported = false; // TODO: Set to true if testing against a version with native reservation support let fileContent = fs.readFileSync(COMPILED_JSON_PATH, 'utf8') From 27006ef26c90e605d3e47d1c8b30af34cdeb4c49 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:07:54 +0100 Subject: [PATCH 18/22] fix createReservationSetter to directly use actionToReservation from preprocessConfig --- index.js | 91 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/index.js b/index.js index c2eae3a..8200fb0 100644 --- a/index.js +++ b/index.js @@ -107,7 +107,7 @@ function preprocessConfig(config) { * // ${reservationSetter(ctx)} */ function createReservationSetter(config) { - const configSets = preprocessConfig(config) + const { actionToReservation } = preprocessConfig(config) return function reservationSetter(ctx) { if (isNativeReservationSupported()) { @@ -117,7 +117,7 @@ function createReservationSetter(config) { const actionName = getActionName(ctx) if (!actionName) return '' - const reservation = findReservation(actionName, configSets) + const reservation = findReservation(actionName, actionToReservation) return reservation ? `SET @@reservation='${reservation}';` @@ -260,65 +260,66 @@ function applyReservationToAction(action, actionToReservation) { // Old Approach (SQL Prepending) const statement = `SET @@reservation='${reservation}';` - // For operation builders, the queries are often set AFTER the builder is created via .queries() - // We monkeypatch the .queries() method to ensure our statement is always prepended. - if (isOperation && typeof action.queries === 'function' && !action._queriesPatched) { - const originalQueriesFn = action.queries - action.queries = function (queries) { - // Check for outer DECLARE before wrapping - if (hasOuterDeclare(queries)) { - return originalQueriesFn.apply(this, [queries]) + // For operation builders, the queries are often set AFTER the builder is created via .queries() + // We monkeypatch the .queries() method to ensure our statement is always prepended. + if (isOperation && typeof action.queries === 'function' && !action._queriesPatched) { + const originalQueriesFn = action.queries + action.queries = function (queries) { + // Check for outer DECLARE before wrapping + if (hasOuterDeclare(queries)) { + return originalQueriesFn.apply(this, [queries]) + } + + const queriesArray = typeof queries === 'function' + ? (ctx) => prependStatement(queries(ctx), statement) + : prependStatement(queries, statement) + return originalQueriesFn.apply(this, [queriesArray]) } - - const queriesArray = typeof queries === 'function' - ? (ctx) => prependStatement(queries(ctx), statement) - : prependStatement(queries, statement) - return originalQueriesFn.apply(this, [queriesArray]) + action._queriesPatched = true } - action._queriesPatched = true - } // Prefer modifying data structure directly if we know it's a safe type // This handles both Builders (via .proto) and Compiled Objects (direct) - // 1. Try contextablePreOps (Tables/Views Builders before resolution) - if (action.contextablePreOps) { - if (!hasOuterDeclare(action.contextablePreOps)) { - action.contextablePreOps = prependStatement(action.contextablePreOps, statement) + // 1. Try contextablePreOps (Tables/Views Builders before resolution) + if (action.contextablePreOps) { + if (!hasOuterDeclare(action.contextablePreOps)) { + action.contextablePreOps = prependStatement(action.contextablePreOps, statement) + } } - } - // 2. Try contextableQueries (Operations Builders before resolution) - else if (action.contextableQueries) { + // 2. Try contextableQueries (Operations Builders before resolution) + else if (action.contextableQueries) { // Skip if there is an outer DECLARE if (!hasOuterDeclare(action.contextableQueries)) { action.contextableQueries = prependStatement(action.contextableQueries, statement) } - } - // 3. Try proto.preOps (Compiled Tables/Views or Resolved Builders) - else if (hasType) { - if (!hasOuterDeclare(proto.preOps || [])) { - if (!proto.preOps) { - proto.preOps = [] + } + // 3. Try proto.preOps (Compiled Tables/Views or Resolved Builders) + else if (hasType) { + if (!hasOuterDeclare(proto.preOps || [])) { + if (!proto.preOps) { + proto.preOps = [] + } + + if (isArrayOrString(proto.preOps)) { + proto.preOps = prependStatement(proto.preOps, statement) + } else if (hasPreOpsFn) { + action.preOps(statement) + } } - - if (isArrayOrString(proto.preOps)) { - proto.preOps = prependStatement(proto.preOps, statement) - } else if (hasPreOpsFn) { - action.preOps(statement) + } + // 4. Try proto.queries (Compiled Operations or Resolved Builders) + else if (proto.queries) { + // Skip if there is an outer DECLARE + if (!hasOuterDeclare(proto.queries)) { + proto.queries = prependStatement(proto.queries, statement) } } - } - // 4. Try proto.queries (Compiled Operations or Resolved Builders) - else if (proto.queries) { - // Skip if there is an outer DECLARE - if (!hasOuterDeclare(proto.queries)) { - proto.queries = prependStatement(proto.queries, statement) + // 5. Fallback to function API (likely Tables/Views) + else if (hasPreOpsFn) { + action.preOps(statement) } } - // 5. Fallback to function API (likely Tables/Views) - else if (hasPreOpsFn) { - action.preOps(statement) - } } } From 0a5377cac9c03c0bcdc44fed05b13ab0e3a45469 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:14:29 +0100 Subject: [PATCH 19/22] refactor createReservationSetter --- index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/index.js b/index.js index 8200fb0..41f1799 100644 --- a/index.js +++ b/index.js @@ -115,13 +115,9 @@ function createReservationSetter(config) { } const actionName = getActionName(ctx) - if (!actionName) return '' - const reservation = findReservation(actionName, actionToReservation) - return reservation - ? `SET @@reservation='${reservation}';` - : '' + return reservation ? `SET @@reservation='${reservation}';` : '' } } From 8ab74e91e01aa7f8fd41489313d804587081a8aa Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:36:57 +0100 Subject: [PATCH 20/22] Update CONTRIBUTING.md and README.md for clarity and additional testing instructions --- CONTRIBUTING.md | 38 +++++++++++++++++++++++++++++--------- README.md | 1 + 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aa05c34..c807def 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,50 +4,70 @@ We welcome contributions to the Dataform package! This document provides guideli ## Development Setup -1. **Clone the repository:** +### 1. **Clone the repository:** ```bash git clone https://github.com/masthead-data/dataform-package.git cd dataform-package ``` -2. **Install dependencies:** +### 2. **Install dependencies:** ```bash npm install ``` -3. **Run tests:** +### 3. **Run tests:** + +#### 3.1 Matrix Testing (Default) - #### Matrix Testing (Default) Run from the root to test all supported versions: + ```bash npm test ``` + This command iterates through all supported Dataform versions (currently v2.4.2 and v3.X.X), managing configuration file conflicts automatically. - #### Single Version (Fast Iteration) +#### 3.2 Single Version (Fast Iteration) + For rapid development on the version currently installed in `test-project`: + ```bash npm run test:single ``` + This runs: 1. `jest`: Unit tests for helper functions. 2. `dataform compile`: Generates the actual project graph. 3. `verify_compilation.js`: In-depth JSON inspection. - #### Specific Version +#### 3.3 Specific Version + Test a single Dataform version: + ```bash npm test -- 2.4.2 ``` -4. **Run linting:** +### 4. **Run linting:** ```bash npm run lint ``` +### Local Integration Testing + +The `test-project` is configured to use the local version of the package. In `test-project/package.json`: + +```json +"dependencies": { + "@masthead-data/dataform-package": "file:../" +} +``` + +**Note:** `npm ci` or `npm install` in the `test-project` caches the local package. If you make changes to `index.js` and don't see them reflected, you may need to force an update or avoid `npm ci` during rapid iteration. + ## Project Structure ```filetree @@ -57,8 +77,7 @@ dataform-package/ │ └── index.test.js # Test suite ├── package.json # Package configuration ├── README.md # Main documentation -├── CHANGELOG.md # Version history -└── .eslintrc.js # ESLint configuration +└── CHANGELOG.md # Version history ``` ## Making Changes @@ -71,6 +90,7 @@ This project uses `npm ci` in CI/CD pipelines, which requires `package-lock.json The project includes optional platform-specific dependencies (e.g., `@unrs/resolver-binding-*`). If you update dependencies on macOS, `npm` might "clean" other platform bindings from the lockfile, causing CI to fail on Linux. If CI fails with `npm error EUSAGE` related to missing platform bindings: + 1. Restore the `package-lock.json` to a known good state. 2. Run `npm install` to update the version/dependencies without removing optional bindings. 3. Verify that the lockfile still contains entries for `@unrs/resolver-binding-linux-*` before committing. diff --git a/README.md b/README.md index 971e43f..10b4d75 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ Actions are matched against the `RESERVATION_CONFIG` using exact string matching Based on the matched reservation, the package automatically prepends the `SET @@reservation` SQL statement to your queries or pre-operations. The specific reservation value applied follows this logic: + * **Specific Reservation**: `projects/{project}/locations/{location}/reservations/{name}` * **On-demand**: `none` * **Default/Null**: No reservation override applied From 654c8f96329d93e92c17b68cfd4ce8fd9b05b55d Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:39:58 +0100 Subject: [PATCH 21/22] Remove duplicated isArrayOrString and prependStatement functions from index.js --- index.js | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/index.js b/index.js index dc51819..3630144 100644 --- a/index.js +++ b/index.js @@ -205,35 +205,6 @@ function prependStatement(target, statement) { return target } -/** - * Checks if a value is an array or string - * @param {any} val - The value to check - * @returns {boolean} True if array or string - */ -function isArrayOrString(val) { - return Array.isArray(val) || typeof val === 'string' -} -/** - * Ensures a statement is prepended to an array or string - * @param {Array|string} target - The target to prepend to - * @param {string} statement - The statement to prepend - * @returns {Array|string} The modified target - */ -function prependStatement(target, statement) { - if (Array.isArray(target)) { - if (!target.includes(statement)) { - return [statement, ...target] - } - return target - } - if (typeof target === 'string') { - if (!target.includes(statement)) { - return [statement, target] - } - } - return target -} - /** * Checks if a value is an array or string * @param {any} val - The value to check From 9d2ded65c2e0e3f24a59f48c9d9cbef95e3ce292 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:41:10 +0100 Subject: [PATCH 22/22] lint --- scripts/verify_compilation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify_compilation.js b/scripts/verify_compilation.js index 57cae8b..bc92d5a 100644 --- a/scripts/verify_compilation.js +++ b/scripts/verify_compilation.js @@ -12,7 +12,7 @@ function verify() { const version = process.argv[2] console.log(`Running verification for Dataform version: ${version}`) - const isNativeSupported = false; // TODO: Set to true if testing against a version with native reservation support + const isNativeSupported = false // TODO: Set to true if testing against a version with native reservation support let fileContent = fs.readFileSync(COMPILED_JSON_PATH, 'utf8')