/**
 * Wire-format representation of the zql AST interface.
 *
 * `v.Type<...>` types are explicitly declared to facilitate Typescript verification
 * that the schemas satisfy the zql type definitions. (Incidentally, explicit types
 * are also required for recursive schema definitions.)
 */
import { compareUTF8 } from 'compare-utf8';
import { defined } from "../../shared/src/arrays.js";
import { assert } from "../../shared/src/asserts.js";
import { must } from "../../shared/src/must.js";
import * as v from "../../shared/src/valita.js";
import { rowSchema } from "./data.js";
export const SUBQ_PREFIX = 'zsubq_';
export const selectorSchema = v.string();
export const toStaticParam = Symbol();
const orderingElementSchema = v.readonly(v.tuple([selectorSchema, v.literalUnion('asc', 'desc')]));
export const orderingSchema = v.readonlyArray(orderingElementSchema);
export const primitiveSchema = v.union(v.string(), v.number(), v.boolean(), v.null());
export const equalityOpsSchema = v.literalUnion('=', '!=', 'IS', 'IS NOT');
export const orderOpsSchema = v.literalUnion('<', '>', '<=', '>=');
export const likeOpsSchema = v.literalUnion('LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE');
export const inOpsSchema = v.literalUnion('IN', 'NOT IN');
export const simpleOperatorSchema = v.union(equalityOpsSchema, orderOpsSchema, likeOpsSchema, inOpsSchema);
const literalReferenceSchema = v.readonlyObject({
    type: v.literal('literal'),
    value: v.union(v.string(), v.number(), v.boolean(), v.null(), v.readonlyArray(v.union(v.string(), v.number(), v.boolean()))),
});
const columnReferenceSchema = v.readonlyObject({
    type: v.literal('column'),
    name: v.string(),
});
/**
 * A parameter is a value that is not known at the time the query is written
 * and is resolved at runtime.
 *
 * Static parameters refer to something provided by the caller.
 * Static parameters are injected when the query pipeline is built from the AST
 * and do not change for the life of that pipeline.
 *
 * An example static parameter is the current authentication data.
 * When a user is authenticated, queries on the server have access
 * to the user's authentication data in order to evaluate authorization rules.
 * Authentication data doesn't change over the life of a query as a change
 * in auth data would represent a log-in / log-out of the user.
 *
 * AncestorParameters refer to rows encountered while running the query.
 * They are used by subqueries to refer to rows emitted by parent queries.
 */
const parameterReferenceSchema = v.readonlyObject({
    type: v.literal('static'),
    // The "namespace" of the injected parameter.
    // Write authorization will send the value of a row
    // prior to the mutation being run (preMutationRow).
    // Read and write authorization will both send the
    // current authentication data (authData).
    anchor: v.literalUnion('authData', 'preMutationRow'),
    field: v.union(v.string(), v.array(v.string())),
});
const conditionValueSchema = v.union(literalReferenceSchema, columnReferenceSchema, parameterReferenceSchema);
export const simpleConditionSchema = v.readonlyObject({
    type: v.literal('simple'),
    op: simpleOperatorSchema,
    left: conditionValueSchema,
    right: v.union(parameterReferenceSchema, literalReferenceSchema),
});
export const correlatedSubqueryConditionOperatorSchema = v.literalUnion('EXISTS', 'NOT EXISTS');
export const correlatedSubqueryConditionSchema = v.readonlyObject({
    type: v.literal('correlatedSubquery'),
    related: v.lazy(() => correlatedSubquerySchema),
    op: correlatedSubqueryConditionOperatorSchema,
    flip: v.boolean().optional(),
});
export const conditionSchema = v.union(simpleConditionSchema, v.lazy(() => conjunctionSchema), v.lazy(() => disjunctionSchema), correlatedSubqueryConditionSchema);
const conjunctionSchema = v.readonlyObject({
    type: v.literal('and'),
    conditions: v.readonlyArray(conditionSchema),
});
const disjunctionSchema = v.readonlyObject({
    type: v.literal('or'),
    conditions: v.readonlyArray(conditionSchema),
});
function mustCompoundKey(field) {
    assert(Array.isArray(field) && field.length >= 1);
    return field;
}
export const compoundKeySchema = v.readonly(v.tuple([v.string()]).concat(v.array(v.string())));
const correlationSchema = v.readonlyObject({
    parentField: compoundKeySchema,
    childField: compoundKeySchema,
});
// Split out so that its inferred type can be checked against
// Omit<CorrelatedSubquery, 'correlation'> in ast-type-test.ts.
// The mutually-recursive reference of the 'other' field to astSchema
// is the only thing added in v.lazy.  The v.lazy is necessary due to the
// mutually-recursive types, but v.lazy prevents inference of the resulting
// type.
export const correlatedSubquerySchemaOmitSubquery = v.readonlyObject({
    correlation: correlationSchema,
    hidden: v.boolean().optional(),
    system: v.literalUnion('permissions', 'client', 'test').optional(),
});
export const correlatedSubquerySchema = correlatedSubquerySchemaOmitSubquery.extend({
    subquery: v.lazy(() => astSchema),
});
export const astSchema = v.readonlyObject({
    schema: v.string().optional(),
    table: v.string(),
    alias: v.string().optional(),
    where: conditionSchema.optional(),
    related: v.readonlyArray(correlatedSubquerySchema).optional(),
    limit: v.number().optional(),
    orderBy: orderingSchema.optional(),
    start: v
        .object({
        row: rowSchema,
        exclusive: v.boolean(),
    })
        .optional(),
});
function transformAST(ast, transform) {
    // Name mapping functions (e.g. to server names)
    const { tableName, columnName } = transform;
    const colName = (c) => columnName(ast.table, c);
    const key = (table, k) => {
        const serverKey = k.map(col => columnName(table, col));
        return mustCompoundKey(serverKey);
    };
    const where = ast.where ? transform.where(ast.where) : undefined;
    const transformed = {
        schema: ast.schema,
        table: tableName(ast.table),
        alias: ast.alias,
        where: where ? transformWhere(where, ast.table, transform) : undefined,
        related: ast.related
            ? transform.related(ast.related.map(r => ({
                correlation: {
                    parentField: key(ast.table, r.correlation.parentField),
                    childField: key(r.subquery.table, r.correlation.childField),
                },
                hidden: r.hidden,
                subquery: transformAST(r.subquery, transform),
                system: r.system,
            })))
            : undefined,
        start: ast.start
            ? {
                ...ast.start,
                row: Object.fromEntries(Object.entries(ast.start.row).map(([col, val]) => [
                    colName(col),
                    val,
                ])),
            }
            : undefined,
        limit: ast.limit,
        orderBy: ast.orderBy?.map(([col, dir]) => [colName(col), dir]),
    };
    return transformed;
}
function transformWhere(where, table, transform) {
    // Name mapping functions (e.g. to server names)
    const { columnName } = transform;
    const condValue = (c) => c.type !== 'column' ? c : { ...c, name: columnName(table, c.name) };
    const key = (table, k) => {
        const serverKey = k.map(col => columnName(table, col));
        return mustCompoundKey(serverKey);
    };
    if (where.type === 'simple') {
        return { ...where, left: condValue(where.left) };
    }
    else if (where.type === 'correlatedSubquery') {
        const { correlation, subquery } = where.related;
        return {
            ...where,
            related: {
                ...where.related,
                correlation: {
                    parentField: key(table, correlation.parentField),
                    childField: key(subquery.table, correlation.childField),
                },
                subquery: transformAST(subquery, transform),
            },
        };
    }
    return {
        type: where.type,
        conditions: transform.conditions(where.conditions.map(c => transformWhere(c, table, transform))),
    };
}
const normalizeCache = new WeakMap();
const NORMALIZE_TRANSFORM = {
    tableName: t => t,
    columnName: (_, c) => c,
    related: sortedRelated,
    where: flattened,
    conditions: c => c.sort(cmpCondition),
};
export function normalizeAST(ast) {
    let normalized = normalizeCache.get(ast);
    if (!normalized) {
        normalized = transformAST(ast, NORMALIZE_TRANSFORM);
        normalizeCache.set(ast, normalized);
    }
    return normalized;
}
export function mapAST(ast, mapper) {
    return transformAST(ast, {
        tableName: table => mapper.tableName(table),
        columnName: (table, col) => mapper.columnName(table, col),
        related: r => r,
        where: w => w,
        conditions: c => c,
    });
}
export function mapCondition(cond, table, mapper) {
    return transformWhere(cond, table, {
        tableName: table => mapper.tableName(table),
        columnName: (table, col) => mapper.columnName(table, col),
        related: r => r,
        where: w => w,
        conditions: c => c,
    });
}
function sortedRelated(related) {
    return related.sort(cmpRelated);
}
function cmpCondition(a, b) {
    if (a.type === 'simple') {
        if (b.type !== 'simple') {
            return -1; // Order SimpleConditions first
        }
        return (compareValuePosition(a.left, b.left) ||
            compareUTF8MaybeNull(a.op, b.op) ||
            compareValuePosition(a.right, b.right));
    }
    if (b.type === 'simple') {
        return 1; // Order SimpleConditions first
    }
    if (a.type === 'correlatedSubquery') {
        if (b.type !== 'correlatedSubquery') {
            return -1; // Order subquery before conjuctions/disjuctions
        }
        return cmpRelated(a.related, b.related) || compareUTF8MaybeNull(a.op, b.op);
    }
    if (b.type === 'correlatedSubquery') {
        return -1; // Order correlatedSubquery before conjuctions/disjuctions
    }
    const val = compareUTF8MaybeNull(a.type, b.type);
    if (val !== 0) {
        return val;
    }
    for (let l = 0, r = 0; l < a.conditions.length && r < b.conditions.length; l++, r++) {
        const val = cmpCondition(a.conditions[l], b.conditions[r]);
        if (val !== 0) {
            return val;
        }
    }
    // prefixes first
    return a.conditions.length - b.conditions.length;
}
function compareValuePosition(a, b) {
    if (a.type !== b.type) {
        return compareUTF8(a.type, b.type);
    }
    switch (a.type) {
        case 'literal':
            assert(b.type === 'literal');
            return compareUTF8(String(a.value), String(b.value));
        case 'column':
            assert(b.type === 'column');
            return compareUTF8(a.name, b.name);
        case 'static':
            throw new Error('Static parameters should be resolved before normalization');
    }
}
function cmpRelated(a, b) {
    return compareUTF8(must(a.subquery.alias), must(b.subquery.alias));
}
/**
 * Returns a flattened version of the Conditions in which nested Conjunctions with
 * the same operation ('AND' or 'OR') are flattened to the same level. e.g.
 *
 * ```
 * ((a AND b) AND (c AND (d OR (e OR f)))) -> (a AND b AND c AND (d OR e OR f))
 * ```
 *
 * Also flattens singleton Conjunctions regardless of operator, and removes
 * empty Conjunctions.
 */
function flattened(cond) {
    if (cond.type === 'simple' || cond.type === 'correlatedSubquery') {
        return cond;
    }
    const conditions = defined(cond.conditions.flatMap(c => c.type === cond.type ? c.conditions.map(c => flattened(c)) : flattened(c)));
    switch (conditions.length) {
        case 0:
            return undefined;
        case 1:
            return conditions[0];
        default:
            return {
                type: cond.type,
                conditions,
            };
    }
}
function compareUTF8MaybeNull(a, b) {
    if (a !== null && b !== null) {
        return compareUTF8(a, b);
    }
    if (b !== null) {
        return -1;
    }
    if (a !== null) {
        return 1;
    }
    return 0;
}
//# sourceMappingURL=ast.js.map