"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/testing/index.ts
var testing_exports = {};
__export(testing_exports, {
  createTestReporter: () => createTestReporter,
  explain: () => explain,
  percentile: () => percentile,
  profileBenchmark: () => profileBenchmark,
  profileQuery: () => profileQuery,
  resolveProfiles: () => resolveProfiles,
  tableStats: () => tableStats
});
module.exports = __toCommonJS(testing_exports);

// src/sqlHelpers.ts
var quoteIdentifier = (name) => {
  return name.startsWith("`") && name.endsWith("`") ? name : `\`${name}\``;
};
var isTable = (value) => typeof value === "object" && value !== null && "kind" in value && value.kind === "OlapTable";
var isView = (value) => typeof value === "object" && value !== null && "kind" in value && value.kind === "View";
var isColumn = (value) => typeof value === "object" && value !== null && !("kind" in value) && "name" in value && "annotations" in value;
function sqlImpl(strings, ...values) {
  return new Sql(strings, values);
}
var sql = sqlImpl;
sql.statement = function(strings, ...values) {
  return new Sql(strings, values, false);
};
sql.fragment = function(strings, ...values) {
  return new Sql(strings, values, true);
};
var instanceofSql = (value) => typeof value === "object" && "values" in value && "strings" in value;
var Sql = class _Sql {
  values;
  strings;
  isFragment;
  constructor(rawStrings, rawValues, isFragment) {
    if (rawStrings.length - 1 !== rawValues.length) {
      if (rawStrings.length === 0) {
        throw new TypeError("Expected at least 1 string");
      }
      throw new TypeError(
        `Expected ${rawStrings.length} strings to have ${rawStrings.length - 1} values`
      );
    }
    const valuesLength = rawValues.reduce(
      (len, value) => len + (instanceofSql(value) ? value.values.length : isColumn(value) || isTable(value) || isView(value) ? 0 : 1),
      0
    );
    this.values = new Array(valuesLength);
    this.strings = new Array(valuesLength + 1);
    this.isFragment = isFragment;
    this.strings[0] = rawStrings[0];
    let i = 0, pos = 0;
    while (i < rawValues.length) {
      const child = rawValues[i++];
      const rawString = rawStrings[i];
      if (instanceofSql(child)) {
        this.strings[pos] += child.strings[0];
        let childIndex = 0;
        while (childIndex < child.values.length) {
          this.values[pos++] = child.values[childIndex++];
          this.strings[pos] = child.strings[childIndex];
        }
        this.strings[pos] += rawString;
      } else if (isColumn(child)) {
        const aggregationFunction = child.annotations.find(
          ([k, _]) => k === "aggregationFunction"
        );
        if (aggregationFunction !== void 0) {
          const funcName = aggregationFunction[1].functionName;
          const parenIdx = funcName.indexOf("(");
          const mergedName = parenIdx !== -1 ? `${funcName.slice(0, parenIdx)}Merge${funcName.slice(parenIdx)}` : `${funcName}Merge`;
          this.strings[pos] += `${mergedName}(\`${child.name}\`)`;
        } else {
          this.strings[pos] += `\`${child.name}\``;
        }
        this.strings[pos] += rawString;
      } else if (isTable(child)) {
        const deployedName = child.generateTableName();
        if (child.config.database) {
          this.strings[pos] += `\`${child.config.database}\`.\`${deployedName}\``;
        } else {
          this.strings[pos] += `\`${deployedName}\``;
        }
        this.strings[pos] += rawString;
      } else if (isView(child)) {
        if (child.database) {
          this.strings[pos] += `\`${child.database}\`.\`${child.name}\``;
        } else {
          this.strings[pos] += `\`${child.name}\``;
        }
        this.strings[pos] += rawString;
      } else {
        this.values[pos++] = child;
        this.strings[pos] = rawString;
      }
    }
  }
  /**
   * Append another Sql fragment, returning a new Sql instance.
   */
  append(other) {
    return new _Sql(
      [...this.strings, ""],
      [...this.values, other],
      this.isFragment
    );
  }
};
sql.join = function(fragments, separator) {
  if (fragments.length === 0) return new Sql([""], [], true);
  if (fragments.length === 1) {
    const frag = fragments[0];
    return new Sql(frag.strings, frag.values, true);
  }
  const sep = separator ?? ", ";
  const normalized = sep.includes(" ") ? sep : ` ${sep} `;
  const strings = ["", ...Array(fragments.length - 1).fill(normalized), ""];
  return new Sql(strings, fragments, true);
};
sql.raw = function(text) {
  return new Sql([text], [], true);
};
var toQueryPreview = (sql2) => {
  try {
    const formatValue = (v) => {
      if (Array.isArray(v)) {
        const [type, val] = v;
        if (type === "Identifier") {
          return `\`${String(val)}\``;
        }
        return `[${v.map((x) => formatValue(x)).join(", ")}]`;
      }
      if (v === null || v === void 0) return "NULL";
      if (typeof v === "string") return `'${v.replace(/'/g, "''")}'`;
      if (typeof v === "number") return String(v);
      if (typeof v === "boolean") return v ? "true" : "false";
      if (v instanceof Date)
        return `'${v.toISOString().replace("T", " ").slice(0, 19)}'`;
      try {
        return JSON.stringify(v);
      } catch {
        return String(v);
      }
    };
    let out = sql2.strings[0] ?? "";
    for (let i = 0; i < sql2.values.length; i++) {
      const val = getValueFromParameter(sql2.values[i]);
      out += formatValue(val);
      out += sql2.strings[i + 1] ?? "";
    }
    return out.replace(/\s+/g, " ").trim();
  } catch (error) {
    console.log(`toQueryPreview error: ${error}`);
    return "/* query preview unavailable */";
  }
};
var getValueFromParameter = (value) => {
  if (Array.isArray(value)) {
    const [type, val] = value;
    if (type === "Identifier") return val;
  }
  return value;
};

// src/testing/shared.ts
function validateRuns(runs) {
  if (!Number.isInteger(runs) || runs < 1) {
    throw new Error(`runs must be a positive integer, got ${runs}`);
  }
}

// src/testing/clickhouse-diagnostics.ts
function percentile(values, p) {
  if (values.length === 0) return 0;
  const sorted = [...values].sort((a, b) => a - b);
  const index = Math.min(
    sorted.length - 1,
    Math.ceil(p / 100 * sorted.length) - 1
  );
  return sorted[Math.max(0, index)];
}
async function explain(queryClient, query) {
  const sqlString = toQueryPreview(query);
  const explainSql = sql.raw(`EXPLAIN indexes = 1 ${sqlString}`);
  const result = await queryClient.execute(explainSql);
  const rows = await result.json();
  const lines = rows.map((r) => r.explain ?? r.EXPLAIN ?? "");
  const full = lines.join("\n");
  const conditionMatch = full.match(/Condition:\s*(.+)/);
  const granulesMatch = full.match(/Granules:\s*(\d+)\/(\d+)/);
  const selected = granulesMatch ? parseInt(granulesMatch[1], 10) : 0;
  const total = granulesMatch ? parseInt(granulesMatch[2], 10) : 0;
  const skipPct = total > 0 ? (total - selected) / total * 100 : 0;
  return {
    indexCondition: conditionMatch?.[1]?.trim() ?? "unknown",
    selectedGranules: selected,
    totalGranules: total,
    granuleSkipPct: Math.round(skipPct),
    rawPlan: full
  };
}
async function profileQuery(queryClient, query) {
  const result = await queryClient.execute(query);
  await result.json();
  return result.query_id;
}
async function resolveProfiles(queryClient, queryIds) {
  if (queryIds.length === 0) return [];
  await queryClient.command(sql.raw("SYSTEM FLUSH LOGS"));
  const idFragments = queryIds.map((id) => sql`${id}`);
  const logQuery = sql`SELECT
    query_id, read_rows, read_bytes, memory_usage, query_duration_ms, result_rows,
    ProfileEvents['DiskReadElapsedMicroseconds'] as disk_read_us,
    ProfileEvents['OSReadChars'] as os_read_bytes
  FROM system.query_log
  WHERE query_id IN (${sql.join(idFragments)}) AND type = 'QueryFinish'
  ORDER BY event_time ASC`;
  const logResult = await queryClient.execute(logQuery);
  const logRows = await logResult.json();
  const byId = new Map(logRows.map((r) => [r.query_id, r]));
  return queryIds.map((id) => {
    const entry = byId.get(id);
    if (!entry) {
      throw new Error(
        `No query_log entry for query_id=${id}. Check log_queries setting.`
      );
    }
    return {
      queryId: id,
      readRows: Number(entry.read_rows ?? 0),
      readBytes: Number(entry.read_bytes ?? 0),
      memoryUsage: Number(entry.memory_usage ?? 0),
      durationMs: Number(entry.query_duration_ms ?? 0),
      resultRows: Number(entry.result_rows ?? 0),
      diskReadUs: Number(entry.disk_read_us ?? 0),
      osReadBytes: Number(entry.os_read_bytes ?? 0)
    };
  });
}
async function profileBenchmark(queryClient, query, runs) {
  validateRuns(runs);
  const queryIds = [];
  for (let i = 0; i < runs; i++) {
    queryIds.push(await profileQuery(queryClient, query));
  }
  const profiles = await resolveProfiles(queryClient, queryIds);
  const durations = profiles.map((p) => p.durationMs);
  return {
    profiles,
    p50: percentile(durations, 50),
    p95: percentile(durations, 95)
  };
}
var TABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
async function tableStats(queryClient, table) {
  if (!TABLE_NAME_RE.test(table)) {
    throw new Error(`Invalid table name: ${table}`);
  }
  const quoted = table.split(".").map((part) => quoteIdentifier(part)).join(".");
  const parts = table.split(".");
  const bareTable = parts.length > 1 ? parts[parts.length - 1] : table;
  const bareDatabase = parts.length > 1 ? parts[0] : null;
  const [countRows, partsRows] = await Promise.all([
    queryClient.execute(sql.raw(`SELECT count() as rows FROM ${quoted}`)).then((r) => r.json()),
    queryClient.execute(
      bareDatabase ? sql`SELECT count() as parts, formatReadableSize(sum(bytes_on_disk)) as disk_size
           FROM system.parts
           WHERE active = 1 AND table = ${bareTable} AND database = ${bareDatabase}` : sql`SELECT count() as parts, formatReadableSize(sum(bytes_on_disk)) as disk_size
           FROM system.parts
           WHERE active = 1 AND table = ${bareTable}`
    ).then((r) => r.json())
  ]);
  return {
    table,
    rows: Number(countRows[0]?.rows ?? 0),
    parts: Number(partsRows[0]?.parts ?? 0),
    diskSize: partsRows[0]?.disk_size ?? "unknown"
  };
}

// src/testing/test-reporter.ts
var import_promises = require("fs/promises");
var import_node_path = require("path");
function createTestReporter(options) {
  const target = options.target ?? {
    host: process.env.MOOSE_CLICKHOUSE_CONFIG__HOST ?? "localhost",
    database: process.env.MOOSE_CLICKHOUSE_CONFIG__DB_NAME ?? "local"
  };
  const results = {
    timestamp: (/* @__PURE__ */ new Date()).toISOString(),
    target,
    tests: {}
  };
  const flush = async () => {
    await (0, import_promises.mkdir)(options.outputDir, { recursive: true });
    const filename = `${options.prefix}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`;
    const filepath = (0, import_node_path.join)(options.outputDir, filename);
    await (0, import_promises.writeFile)(filepath, JSON.stringify(results, null, 2));
    return filepath;
  };
  return { results, flush };
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  createTestReporter,
  explain,
  percentile,
  profileBenchmark,
  profileQuery,
  resolveProfiles,
  tableStats
});
//# sourceMappingURL=index.js.map