const getTypeOf = (thing) => typeof thing;
const TINYBASE = 'tinybase';
const EMPTY_STRING = '';
const COMMA = ',';
const DOT = '.';
const STRING = getTypeOf(EMPTY_STRING);
const TRUE = 'true';
const UNDEFINED = '\uFFFC';
const strMatch = (str, regex) => str?.match(regex);
const strSplit = (str, separator = EMPTY_STRING, limit) =>
  str.split(separator, limit);
const strReplace = (str, searchValue, replaceValue) =>
  str.replace(searchValue, replaceValue);

const promise = Promise;
const getIfNotFunction = (predicate) => (value, then, otherwise) =>
  predicate(value) ? otherwise?.() : then(value);
const GLOBAL = globalThis;
const THOUSAND = 1e3;
const startInterval = (callback, sec, immediate) => {
  return setInterval(callback, sec * THOUSAND);
};
const stopInterval = clearInterval;
const isNullish = (thing) => thing == null;
const isUndefined = (thing) => thing === void 0;
const isNull = (thing) => thing === null;
const isTrue = (thing) => thing === true;
const isFalse = (thing) => thing === false;
const ifNotNullish = getIfNotFunction(isNullish);
const ifNotUndefined = getIfNotFunction(isUndefined);
const isString = (thing) => getTypeOf(thing) == STRING;
const isArray = (thing) => Array.isArray(thing);
const slice = (arrayOrString, start, end) => arrayOrString.slice(start, end);
const size = (arrayOrString) => arrayOrString.length;
const test = (regex, subject) => regex.test(subject);
const promiseAll = async (promises) => promise.all(promises);
const errorNew = (message) => {
  throw new Error(message);
};
const tryCatch = async (action, then1, then2) => {
  try {
    return await action();
  } catch (error) {
    /* istanbul ignore next */
    then1?.(error);
  }
};

const arrayForEach = (array, cb) => array.forEach(cb);
const arrayJoin = (array, sep = EMPTY_STRING) => array.join(sep);
const arrayMap = (array, cb) => array.map(cb);
const arrayIsEmpty = (array) => size(array) == 0;
const arrayFilter = (array, cb) => array.filter(cb);
const arrayClear = (array, to) => array.splice(0, to);
const arrayPush = (array, ...values) => array.push(...values);
const arrayShift = (array) => array.shift();

const collSize = (coll) => coll?.size ?? 0;
const collHas = (coll, keyOrValue) => coll?.has(keyOrValue) ?? false;
const collIsEmpty = (coll) => isUndefined(coll) || collSize(coll) == 0;
const collValues = (coll) => [...(coll?.values() ?? [])];
const collClear = (coll) => coll.clear();
const collForEach = (coll, cb) => coll?.forEach(cb);
const collDel = (coll, keyOrValue) => coll?.delete(keyOrValue);

const object = Object;
const getPrototypeOf = (obj) => object.getPrototypeOf(obj);
const objEntries = object.entries;
const isObject = (obj) =>
  !isNullish(obj) &&
  ifNotNullish(
    getPrototypeOf(obj),
    (objPrototype) =>
      objPrototype == object.prototype ||
      isNullish(getPrototypeOf(objPrototype)),

    /* istanbul ignore next */
    () => true,
  );
const objIds = object.keys;
const objFreeze = object.freeze;
const objNew = (entries = []) => object.fromEntries(entries);
const objMerge = (...objs) => object.assign({}, ...objs);
const objHas = (obj, id) => id in obj;
const objDel = (obj, id) => {
  delete obj[id];
  return obj;
};
const objToArray = (obj, cb) =>
  arrayMap(objEntries(obj), ([id, value]) => cb(value, id));
const objMap = (obj, cb) =>
  objNew(objToArray(obj, (value, id) => [id, cb(value, id)]));
const objValues = (obj) => object.values(obj);
const objSize = (obj) => size(objIds(obj));
const objIsEmpty = (obj) => isObject(obj) && objSize(obj) == 0;

const mapNew = (entries) => new Map(entries);
const mapGet = (map, key) => map?.get(key);
const mapForEach = (map, cb) =>
  collForEach(map, (value, key) => cb(key, value));
const mapMap = (coll, cb) =>
  arrayMap([...(coll?.entries() ?? [])], ([key, value]) => cb(value, key));
const mapSet = (map, key, value) =>
  isUndefined(value) ? (collDel(map, key), map) : map?.set(key, value);
const mapEnsure = (map, key, getDefaultValue, hadExistingValue) => {
  if (!collHas(map, key)) {
    mapSet(map, key, getDefaultValue());
  } else {
    hadExistingValue?.(mapGet(map, key));
  }
  return mapGet(map, key);
};
const visitTree = (node, path, ensureLeaf, pruneLeaf, p = 0) =>
  ifNotUndefined(
    (ensureLeaf ? mapEnsure : mapGet)(
      node,
      path[p],
      p > size(path) - 2 ? ensureLeaf : mapNew,
    ),
    (nodeOrLeaf) => {
      if (p > size(path) - 2) {
        if (pruneLeaf?.(nodeOrLeaf)) {
          mapSet(node, path[p]);
        }
        return nodeOrLeaf;
      }
      const leaf = visitTree(nodeOrLeaf, path, ensureLeaf, pruneLeaf, p + 1);
      if (collIsEmpty(nodeOrLeaf)) {
        mapSet(node, path[p]);
      }
      return leaf;
    },
  );

const INTEGER = /^\d+$/;
const getPoolFunctions = () => {
  const pool = [];
  let nextId = 0;
  return [
    (reuse) => (reuse ? arrayShift(pool) : null) ?? EMPTY_STRING + nextId++,
    (id) => {
      if (test(INTEGER, id) && size(pool) < 1e3) {
        arrayPush(pool, id);
      }
    },
  ];
};

const setNew = (entryOrEntries) =>
  new Set(
    isArray(entryOrEntries) || isUndefined(entryOrEntries)
      ? entryOrEntries
      : [entryOrEntries],
  );
const setAdd = (set, value) => set?.add(value);

const getWildcardedLeaves = (deepIdSet, path = [EMPTY_STRING]) => {
  const leaves = [];
  const deep = (node, p) =>
    p == size(path)
      ? arrayPush(leaves, node)
      : isNull(path[p])
        ? collForEach(node, (node2) => deep(node2, p + 1))
        : arrayForEach([path[p], null], (id) => deep(mapGet(node, id), p + 1));
  deep(deepIdSet, 0);
  return leaves;
};
const getListenerFunctions = (getThing) => {
  let thing;
  const [getId, releaseId] = getPoolFunctions();
  const allListeners = mapNew();
  const addListener = (
    listener,
    idSetNode,
    path,
    pathGetters = [],
    extraArgsGetter = () => [],
  ) => {
    thing ??= getThing();
    const id = getId(1);
    mapSet(allListeners, id, [
      listener,
      idSetNode,
      path,
      pathGetters,
      extraArgsGetter,
    ]);
    setAdd(visitTree(idSetNode, path ?? [EMPTY_STRING], setNew), id);
    return id;
  };
  const callListeners = (idSetNode, ids, ...extraArgs) =>
    arrayForEach(getWildcardedLeaves(idSetNode, ids), (set) =>
      collForEach(set, (id) =>
        mapGet(allListeners, id)[0](thing, ...(ids ?? []), ...extraArgs),
      ),
    );
  const delListener = (id) =>
    ifNotUndefined(mapGet(allListeners, id), ([, idSetNode, idOrNulls]) => {
      visitTree(idSetNode, idOrNulls ?? [EMPTY_STRING], void 0, (idSet) => {
        collDel(idSet, id);
        return collIsEmpty(idSet) ? 1 : 0;
      });
      mapSet(allListeners, id);
      releaseId(id);
      return idOrNulls;
    });
  const callListener = (id) =>
    ifNotUndefined(
      mapGet(allListeners, id),
      ([listener, , path = [], pathGetters, extraArgsGetter]) => {
        const callWithIds = (...ids) => {
          const index = size(ids);
          if (index == size(path)) {
            listener(thing, ...ids, ...extraArgsGetter(ids));
          } else if (isNull(path[index])) {
            arrayForEach(pathGetters[index]?.(...ids) ?? [], (id2) =>
              callWithIds(...ids, id2),
            );
          } else {
            callWithIds(...ids, path[index]);
          }
        };
        callWithIds();
      },
    );
  return [addListener, callListeners, delListener, callListener];
};

const Status = {
  Idle: 0 /* Idle */,
  Loading: 1 /* Loading */,
  Saving: 2 /* Saving */,
};
const Persists = {
  StoreOnly: 1 /* StoreOnly */,
  MergeableStoreOnly: 2 /* MergeableStoreOnly */,
  StoreOrMergeableStore: 3 /* StoreOrMergeableStore */,
};
const scheduleRunning = mapNew();
const scheduleActions = mapNew();
const getStoreFunctions = (
  persist = 1 /* StoreOnly */,
  store,
  isSynchronizer,
) =>
  persist != 1 /* StoreOnly */ && store.isMergeable()
    ? [
        1,
        store.getMergeableContent,
        () => store.getTransactionMergeableChanges(!isSynchronizer),
        ([[changedTables], [changedValues]]) =>
          !objIsEmpty(changedTables) || !objIsEmpty(changedValues),
        store.setDefaultContent,
      ]
    : persist != 2 /* MergeableStoreOnly */
      ? [
          0,
          store.getContent,
          store.getTransactionChanges,
          ([changedTables, changedValues]) =>
            !objIsEmpty(changedTables) || !objIsEmpty(changedValues),
          store.setContent,
        ]
      : errorNew('Store type not supported by this Persister');
const createCustomPersister = (
  store,
  getPersisted,
  setPersisted,
  addPersisterListener,
  delPersisterListener,
  onIgnoredError,
  persist,
  extra = {},
  isSynchronizer = 0,
  scheduleId = [],
) => {
  let status = 0; /* Idle */
  let loads = 0;
  let saves = 0;
  let action;
  let autoLoadHandle;
  let autoSaveListenerId;
  mapEnsure(scheduleRunning, scheduleId, () => 0);
  mapEnsure(scheduleActions, scheduleId, () => []);
  const statusListeners = mapNew();
  const [
    isMergeableStore,
    getContent,
    getChanges,
    hasChanges,
    setDefaultContent,
  ] = getStoreFunctions(persist, store, isSynchronizer);
  const [addListener, callListeners, delListenerImpl] = getListenerFunctions(
    () => persister,
  );
  const setStatus = (newStatus) => {
    if (newStatus != status) {
      status = newStatus;
      callListeners(statusListeners, void 0, status);
    }
  };
  const run = async () => {
    /* istanbul ignore else */
    if (!mapGet(scheduleRunning, scheduleId)) {
      mapSet(scheduleRunning, scheduleId, 1);
      while (
        !isUndefined((action = arrayShift(mapGet(scheduleActions, scheduleId))))
      ) {
        await tryCatch(action, onIgnoredError);
      }
      mapSet(scheduleRunning, scheduleId, 0);
    }
  };
  const setContentOrChanges = (contentOrChanges) => {
    (isMergeableStore && isArray(contentOrChanges?.[0])
      ? contentOrChanges?.[2] === 1
        ? store.applyMergeableChanges
        : store.setMergeableContent
      : contentOrChanges?.[2] === 1
        ? store.applyChanges
        : store.setContent)(contentOrChanges);
  };
  const load = async (initialContent) => {
    /* istanbul ignore else */
    if (status != 2 /* Saving */) {
      setStatus(1 /* Loading */);
      loads++;
      await schedule(async () => {
        await tryCatch(
          async () => {
            const content = await getPersisted();
            if (isArray(content)) {
              setContentOrChanges(content);
            } else if (initialContent) {
              setDefaultContent(initialContent);
            } else {
              errorNew(`Content is not an array: ${content}`);
            }
          },
          () => {
            if (initialContent) {
              setDefaultContent(initialContent);
            }
          },
        );
        setStatus(0 /* Idle */);
      });
    }
    return persister;
  };
  const startAutoLoad = async (initialContent) => {
    stopAutoLoad();
    await load(initialContent);
    await tryCatch(
      async () =>
        (autoLoadHandle = await addPersisterListener(
          async (content, changes) => {
            if (changes || content) {
              /* istanbul ignore else */
              if (status != 2 /* Saving */) {
                setStatus(1 /* Loading */);
                loads++;
                setContentOrChanges(changes ?? content);
                setStatus(0 /* Idle */);
              }
            } else {
              await load();
            }
          },
        )),
      onIgnoredError,
    );
    return persister;
  };
  const stopAutoLoad = async () => {
    if (autoLoadHandle) {
      await tryCatch(
        () => delPersisterListener(autoLoadHandle),
        onIgnoredError,
      );
      autoLoadHandle = void 0;
    }
    return persister;
  };
  const isAutoLoading = () => !isUndefined(autoLoadHandle);
  const save = async (changes) => {
    /* istanbul ignore else */
    if (status != 1 /* Loading */) {
      setStatus(2 /* Saving */);
      saves++;
      await schedule(async () => {
        await tryCatch(() => setPersisted(getContent, changes), onIgnoredError);
        setStatus(0 /* Idle */);
      });
    }
    return persister;
  };
  const startAutoSave = async () => {
    stopAutoSave();
    await save();
    autoSaveListenerId = store.addDidFinishTransactionListener(() => {
      const changes = getChanges();
      if (hasChanges(changes)) {
        save(changes);
      }
    });
    return persister;
  };
  const stopAutoSave = async () => {
    if (autoSaveListenerId) {
      store.delListener(autoSaveListenerId);
      autoSaveListenerId = void 0;
    }
    return persister;
  };
  const isAutoSaving = () => !isUndefined(autoSaveListenerId);
  const startAutoPersisting = async (
    initialContent,
    startSaveFirst = false,
  ) => {
    const [call1, call2] = startSaveFirst
      ? [startAutoSave, startAutoLoad]
      : [startAutoLoad, startAutoSave];
    await call1(initialContent);
    await call2(initialContent);
    return persister;
  };
  const stopAutoPersisting = async (stopSaveFirst = false) => {
    const [call1, call2] = stopSaveFirst
      ? [stopAutoSave, stopAutoLoad]
      : [stopAutoLoad, stopAutoSave];
    await call1();
    await call2();
    return persister;
  };
  const getStatus = () => status;
  const addStatusListener = (listener) =>
    addListener(listener, statusListeners);
  const delListener = (listenerId) => {
    delListenerImpl(listenerId);
    return store;
  };
  const schedule = async (...actions) => {
    arrayPush(mapGet(scheduleActions, scheduleId), ...actions);
    await run();
    return persister;
  };
  const getStore = () => store;
  const destroy = () => {
    arrayClear(mapGet(scheduleActions, scheduleId));
    return stopAutoPersisting();
  };
  const getStats = () => ({loads, saves});
  const persister = {
    load,
    startAutoLoad,
    stopAutoLoad,
    isAutoLoading,
    save,
    startAutoSave,
    stopAutoSave,
    isAutoSaving,
    startAutoPersisting,
    stopAutoPersisting,
    getStatus,
    addStatusListener,
    delListener,
    schedule,
    getStore,
    destroy,
    getStats,
    ...extra,
  };
  return objFreeze(persister);
};

const jsonString = JSON.stringify;
const jsonParse = JSON.parse;
const jsonStringWithUndefined = (obj) =>
  jsonString(obj, (_key, value) => (isUndefined(value) ? UNDEFINED : value));
const jsonParseWithUndefined = (str) =>
  jsonParse(str, (_key, value) => (value === UNDEFINED ? void 0 : value));

const textEncoder = /* @__PURE__ */ new GLOBAL.TextEncoder();
const getHash = (string) => {
  let hash = 2166136261;
  arrayForEach(textEncoder.encode(string), (char) => {
    hash ^= char;
    hash +=
      (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
  });
  return hash >>> 0;
};

const SINGLE_ROW_ID = '_';
const DEFAULT_ROW_ID_COLUMN_NAME = '_id';
const SELECT = 'SELECT';
const WHERE = 'WHERE';
const TABLE = 'TABLE';
const INSERT = 'INSERT';
const DELETE = 'DELETE';
const UPDATE = 'UPDATE';
const ALTER_TABLE = 'ALTER ' + TABLE;
const FROM = 'FROM';
const DELETE_FROM = DELETE + ' ' + FROM;
const SELECT_STAR_FROM = SELECT + '*' + FROM;
const PRAGMA = 'pragma_';
const DATA_VERSION = 'data_version';
const SCHEMA_VERSION = 'schema_version';
const PRAGMA_TABLE = 'pragma_table_';
const CREATE = 'CREATE ';
const CREATE_TABLE = CREATE + TABLE;
const OR_REPLACE = 'OR REPLACE ';
const FUNCTION = 'FUNCTION';
const TABLE_NAME_PLACEHOLDER = '$tableName';
const getWrappedCommand = (executeCommand, onSqlCommand) =>
  onSqlCommand
    ? async (sql, params) => {
        onSqlCommand(sql, params);
        return await executeCommand(sql, params);
      }
    : executeCommand;
const escapeId = (str) =>
  arrayJoin(
    arrayMap(strSplit(str, DOT), (part) => `"${strReplace(part, /"/g, '""')}"`),
    DOT,
  );
const escapeIds = (...ids) => escapeId(arrayJoin(ids, '_'));
const escapeColumnNames = (...columnNames) =>
  arrayJoin(arrayMap(columnNames, escapeId), COMMA);
const getPlaceholders = (array, offset = [1]) =>
  arrayJoin(
    arrayMap(array, () => '$' + offset[0]++),
    COMMA,
  );
const getWhereCondition = (tableName, condition = TRUE) =>
  WHERE +
  `(${strReplace(condition, TABLE_NAME_PLACEHOLDER, escapeId(tableName))})`;

const COLUMN_NAME = 'ColumnName';
const STORE = 'store';
const JSON$1 = 'json';
const STORE_TABLE_NAME = STORE + 'TableName';
const STORE_ID_COLUMN_NAME = STORE + 'Id' + COLUMN_NAME;
const STORE_COLUMN_NAME = STORE + COLUMN_NAME;
const AUTO_LOAD_INTERVAL_SECONDS = 'autoLoadIntervalSeconds';
const ROW_ID_COLUMN_NAME = 'rowId' + COLUMN_NAME;
const TABLE_ID = 'tableId';
const TABLE_NAME = 'tableName';
const DELETE_EMPTY_COLUMNS = 'deleteEmptyColumns';
const DELETE_EMPTY_TABLE = 'deleteEmptyTable';
const CONDITION = 'condition';
const DEFAULT_CONFIG = {
  mode: JSON$1,
  [AUTO_LOAD_INTERVAL_SECONDS]: 1,
};
const DEFAULT_TABULAR_VALUES_CONFIG = {
  load: 0,
  save: 0,
  [TABLE_NAME]: TINYBASE + '_values',
};
const getDefaultedConfig = (configOrStoreTableName) =>
  objMerge(
    DEFAULT_CONFIG,
    isString(configOrStoreTableName)
      ? {[STORE_TABLE_NAME]: configOrStoreTableName}
      : (configOrStoreTableName ?? {}),
  );
const getDefaultedTabularConfigMap = (
  configsObj,
  defaultObj,
  tableField,
  exclude,
  then,
) => {
  const configMap = mapNew();
  objMap(configsObj, (configObj, id) => {
    const defaultedConfig = slice(
      objValues(
        objMerge(
          defaultObj,
          isString(configObj) ? {[tableField]: configObj} : configObj,
        ),
      ),
      0,
      objSize(defaultObj),
    );
    if (!isNull(defaultedConfig[0]) && !exclude(id, defaultedConfig[0])) {
      then(id, defaultedConfig[0]);
      mapSet(configMap, id, defaultedConfig);
    }
  });
  return configMap;
};
const getConfigStructures = (configOrStoreTableName) => {
  const config = getDefaultedConfig(configOrStoreTableName);
  const autoLoadIntervalSeconds = config[AUTO_LOAD_INTERVAL_SECONDS];
  if (config.mode == JSON$1) {
    const storeTableName = config[STORE_TABLE_NAME] ?? TINYBASE;
    return [
      1,
      autoLoadIntervalSeconds,
      [
        storeTableName,
        config[STORE_ID_COLUMN_NAME] ?? DEFAULT_ROW_ID_COLUMN_NAME,
        config[STORE_COLUMN_NAME] ?? STORE,
      ],
      setNew(storeTableName),
    ];
  }
  const {tables: {load = {}, save = {}} = {}, values = {}} = config;
  const valuesConfig = slice(
    objValues(objMerge(DEFAULT_TABULAR_VALUES_CONFIG, values)),
    0,
    objSize(DEFAULT_TABULAR_VALUES_CONFIG),
  );
  const valuesTable = valuesConfig[2];
  const managedTableNames = setNew(valuesTable);
  const excludedTableNames = setNew(valuesTable);
  const tablesLoadConfig = getDefaultedTabularConfigMap(
    load,
    {
      [TABLE_ID]: null,
      [ROW_ID_COLUMN_NAME]: DEFAULT_ROW_ID_COLUMN_NAME,
      [CONDITION]: TRUE,
    },
    TABLE_ID,
    (tableName) => collHas(excludedTableNames, tableName),
    (tableName) => setAdd(managedTableNames, tableName),
  );
  const tablesSaveConfig = getDefaultedTabularConfigMap(
    save,
    {
      [TABLE_NAME]: null,
      [ROW_ID_COLUMN_NAME]: DEFAULT_ROW_ID_COLUMN_NAME,
      [DELETE_EMPTY_COLUMNS]: 0,
      [DELETE_EMPTY_TABLE]: 0,
      [CONDITION]: null,
    },
    TABLE_NAME,
    (_, tableName) => collHas(excludedTableNames, tableName),
    (_, tableName) => setAdd(managedTableNames, tableName),
  );
  mapForEach(
    tablesSaveConfig,
    (_, tableSaveConfig) =>
      (tableSaveConfig[4] ??=
        mapGet(tablesLoadConfig, tableSaveConfig[0])?.[2] ?? TRUE),
  );
  return [
    0,
    autoLoadIntervalSeconds,
    [tablesLoadConfig, tablesSaveConfig, valuesConfig],
    managedTableNames,
  ];
};

const getCommandFunctions = (
  databaseExecuteCommand,
  managedTableNames,
  querySchema,
  onIgnoredError,
  columnType,
  upsert = defaultUpsert,
  encode,
  decode,
) => {
  const schemaMap = mapNew();
  const canSelect = (tableName, rowIdColumnName) =>
    collHas(mapGet(schemaMap, tableName), rowIdColumnName);
  const refreshSchema = async () => {
    collClear(schemaMap);
    arrayMap(
      await querySchema(databaseExecuteCommand, managedTableNames),
      ({tn, cn}) => setAdd(mapEnsure(schemaMap, tn, setNew), cn),
    );
  };
  const loadTable = async (tableName, rowIdColumnName, condition) =>
    canSelect(tableName, rowIdColumnName)
      ? objNew(
          arrayFilter(
            arrayMap(
              await databaseExecuteCommand(
                SELECT_STAR_FROM +
                  escapeId(tableName) +
                  getWhereCondition(tableName, condition),
              ),
              (row) => [
                row[rowIdColumnName],
                decode
                  ? objMap(objDel(row, rowIdColumnName), decode)
                  : objDel(row, rowIdColumnName),
              ],
            ),
            ([rowId, row]) => !isUndefined(rowId) && !objIsEmpty(row),
          ),
        )
      : {};
  const saveTable = async (
    tableName,
    rowIdColumnName,
    content,
    deleteEmptyColumns,
    deleteEmptyTable,
    partial = false,
    condition = TRUE,
  ) => {
    const settingColumnNameSet = setNew();
    objMap(content ?? {}, (contentRow) =>
      arrayMap(objIds(contentRow ?? {}), (cellOrValueId) =>
        setAdd(settingColumnNameSet, cellOrValueId),
      ),
    );
    const settingColumnNames = collValues(settingColumnNameSet);
    if (
      !partial &&
      deleteEmptyTable &&
      condition == TRUE &&
      arrayIsEmpty(settingColumnNames) &&
      collHas(schemaMap, tableName)
    ) {
      await databaseExecuteCommand('DROP ' + TABLE + escapeId(tableName));
      mapSet(schemaMap, tableName);
      return;
    }
    const currentColumnNames = mapGet(schemaMap, tableName);
    const unaccountedColumnNames = setNew(collValues(currentColumnNames));
    if (!arrayIsEmpty(settingColumnNames)) {
      if (!collHas(schemaMap, tableName)) {
        await databaseExecuteCommand(
          CREATE_TABLE +
            escapeId(tableName) +
            `(${escapeId(rowIdColumnName)}${columnType} PRIMARY KEY${arrayJoin(
              arrayMap(
                settingColumnNames,
                (settingColumnName) =>
                  COMMA + escapeId(settingColumnName) + columnType,
              ),
            )});`,
        );
        mapSet(
          schemaMap,
          tableName,
          setNew([rowIdColumnName, ...settingColumnNames]),
        );
      } else {
        await promiseAll(
          arrayMap(
            [rowIdColumnName, ...settingColumnNames],
            async (settingColumnName, index) => {
              if (!collDel(unaccountedColumnNames, settingColumnName)) {
                await databaseExecuteCommand(
                  ALTER_TABLE +
                    escapeId(tableName) +
                    'ADD' +
                    escapeId(settingColumnName) +
                    columnType,
                );
                if (index == 0) {
                  await databaseExecuteCommand(
                    'CREATE UNIQUE INDEX pk ON ' +
                      escapeId(tableName) +
                      `(${escapeId(rowIdColumnName)})`,
                  );
                }
                setAdd(currentColumnNames, settingColumnName);
              }
            },
          ),
        );
      }
    }
    await promiseAll([
      ...(!partial && deleteEmptyColumns
        ? arrayMap(
            collValues(unaccountedColumnNames),
            async (unaccountedColumnName) => {
              if (unaccountedColumnName != rowIdColumnName) {
                await databaseExecuteCommand(
                  ALTER_TABLE +
                    escapeId(tableName) +
                    'DROP' +
                    escapeId(unaccountedColumnName),
                );
                collDel(currentColumnNames, unaccountedColumnName);
              }
            },
          )
        : []),
    ]);
    if (partial) {
      if (isUndefined(content)) {
        await databaseExecuteCommand(
          DELETE_FROM +
            escapeId(tableName) +
            getWhereCondition(tableName, condition),
        );
      } else {
        await promiseAll(
          objToArray(content, async (row, rowId) => {
            if (isUndefined(row)) {
              await databaseExecuteCommand(
                DELETE_FROM +
                  escapeId(tableName) +
                  getWhereCondition(tableName, condition) +
                  `AND(${escapeId(rowIdColumnName)}=$1)`,
                [rowId],
              );
            } else if (!arrayIsEmpty(settingColumnNames)) {
              await upsert(
                databaseExecuteCommand,
                tableName,
                rowIdColumnName,
                objIds(row),
                {
                  [rowId]: encode
                    ? arrayMap(objValues(row), encode)
                    : objValues(row),
                },
                currentColumnNames,
              );
            }
          }),
        );
      }
    } else {
      if (!arrayIsEmpty(settingColumnNames)) {
        const changingColumnNames = arrayFilter(
          collValues(mapGet(schemaMap, tableName)),
          (changingColumnName) => changingColumnName != rowIdColumnName,
        );
        const rows = {};
        const deleteRowIds = [];
        objMap(content ?? {}, (row, rowId) => {
          rows[rowId] = arrayMap(changingColumnNames, (cellId) =>
            encode ? encode(row?.[cellId]) : row?.[cellId],
          );
          arrayPush(deleteRowIds, rowId);
        });
        await upsert(
          databaseExecuteCommand,
          tableName,
          rowIdColumnName,
          changingColumnNames,
          rows,
        );
        await databaseExecuteCommand(
          DELETE_FROM +
            escapeId(tableName) +
            getWhereCondition(tableName, condition) + // eslint-disable-next-line max-len
            `AND${escapeId(rowIdColumnName)}NOT IN(${getPlaceholders(deleteRowIds)})`,
          deleteRowIds,
        );
      } else if (collHas(schemaMap, tableName)) {
        await databaseExecuteCommand(
          DELETE_FROM +
            escapeId(tableName) +
            getWhereCondition(tableName, condition),
        );
      }
    }
  };
  const transaction = async (actions) => {
    let result;
    await databaseExecuteCommand('BEGIN');
    await tryCatch(async () => (result = await actions()), onIgnoredError);
    await databaseExecuteCommand('END');
    return result;
  };
  return [refreshSchema, loadTable, saveTable, transaction];
};
const defaultUpsert = async (
  executeCommand,
  tableName,
  rowIdColumnName,
  changingColumnNames,
  rows,
) => {
  const offset = [1];
  await executeCommand(
    INSERT +
      ' INTO' +
      escapeId(tableName) +
      '(' +
      escapeColumnNames(rowIdColumnName, ...changingColumnNames) +
      ')VALUES' +
      arrayJoin(
        objToArray(
          rows,
          (row) =>
            '($' + offset[0]++ + ',' + getPlaceholders(row, offset) + ')',
        ),
        COMMA,
      ) +
      'ON CONFLICT(' +
      escapeId(rowIdColumnName) +
      `)DO ${UPDATE} SET` +
      arrayJoin(
        arrayMap(
          changingColumnNames,
          (columnName) =>
            escapeId(columnName) + '=excluded.' + escapeId(columnName),
        ),
        COMMA,
      ),
    objToArray(rows, (row, id) => [
      id,
      ...arrayMap(row, (value) => value ?? null),
    ]).flat(),
  );
};

const createJsonPersister = (
  store,
  executeCommand,
  addPersisterListener,
  delPersisterListener,
  onIgnoredError,
  extraDestroy,
  persist,
  [storeTableName, storeIdColumnName, storeColumnName],
  managedTableNames,
  querySchema,
  thing,
  getThing,
  columnType,
  upsert,
) => {
  const [refreshSchema, loadTable, saveTable, transaction] =
    getCommandFunctions(
      executeCommand,
      managedTableNames,
      querySchema,
      onIgnoredError,
      columnType,
      upsert,
    );
  const getPersisted = () =>
    transaction(async () => {
      await refreshSchema();
      return jsonParseWithUndefined(
        (await loadTable(storeTableName, storeIdColumnName))[SINGLE_ROW_ID]?.[
          storeColumnName
        ] ?? 'null',
      );
    });
  const setPersisted = (getContent) =>
    transaction(async () => {
      await refreshSchema();
      await saveTable(
        storeTableName,
        storeIdColumnName,
        {
          [SINGLE_ROW_ID]: {
            [storeColumnName]: jsonStringWithUndefined(getContent() ?? null),
          },
        },
        true,
        true,
      );
    });
  const destroy = async () => {
    await persister.stopAutoPersisting();
    extraDestroy();
    return persister;
  };
  const persister = createCustomPersister(
    store,
    getPersisted,
    setPersisted,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    persist,
    {[getThing]: () => thing, destroy},
    0,
    thing,
  );
  return persister;
};

const createTabularPersister = (
  store,
  executeCommand,
  addPersisterListener,
  delPersisterListener,
  onIgnoredError,
  extraDestroy,
  persist,
  [
    tablesLoadConfig,
    tablesSaveConfig,
    [valuesLoad, valuesSave, valuesTableName],
  ],
  managedTableNames,
  querySchema,
  thing,
  getThing,
  columnType,
  upsert,
  encode,
  decode,
) => {
  const [refreshSchema, loadTable, saveTable, transaction] =
    getCommandFunctions(
      executeCommand,
      managedTableNames,
      querySchema,
      onIgnoredError,
      columnType,
      upsert,
      encode,
      decode,
    );
  const saveTables = (tables, partial) =>
    promiseAll(
      mapMap(
        tablesSaveConfig,
        async (
          [
            tableName,
            rowIdColumnName,
            deleteEmptyColumns,
            deleteEmptyTable,
            condition,
          ],
          tableId,
        ) => {
          if (!partial || objHas(tables, tableId)) {
            await saveTable(
              tableName,
              rowIdColumnName,
              tables[tableId],
              deleteEmptyColumns,
              deleteEmptyTable,
              partial,
              condition,
            );
          }
        },
      ),
    );
  const saveValues = async (values, partial) =>
    valuesSave
      ? await saveTable(
          valuesTableName,
          DEFAULT_ROW_ID_COLUMN_NAME,
          {[SINGLE_ROW_ID]: values},
          true,
          true,
          partial,
        )
      : null;
  const loadTables = async () =>
    objNew(
      arrayFilter(
        await promiseAll(
          mapMap(
            tablesLoadConfig,
            async ([tableId, rowIdColumnName, condition], tableName) => [
              tableId,
              await loadTable(tableName, rowIdColumnName, condition),
            ],
          ),
        ),
        (pair) => !objIsEmpty(pair[1]),
      ),
    );
  const loadValues = async () =>
    valuesLoad
      ? (await loadTable(valuesTableName, DEFAULT_ROW_ID_COLUMN_NAME))[
          SINGLE_ROW_ID
        ]
      : {};
  const getPersisted = () =>
    transaction(async () => {
      await refreshSchema();
      const tables = await loadTables();
      const values = await loadValues();
      return !objIsEmpty(tables) || !isUndefined(values)
        ? [tables, values]
        : void 0;
    });
  const setPersisted = (getContent, changes) =>
    transaction(async () => {
      await refreshSchema();
      if (!isUndefined(changes)) {
        await saveTables(changes[0], true);
        await saveValues(changes[1], true);
      } else {
        const [tables, values] = getContent();
        await saveTables(tables);
        await saveValues(values);
      }
    });
  const destroy = async () => {
    await persister.stopAutoPersisting();
    extraDestroy();
    return persister;
  };
  const persister = createCustomPersister(
    store,
    getPersisted,
    setPersisted,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    persist,
    {[getThing]: () => thing, destroy},
    0,
    thing,
  );
  return persister;
};

const TABLE_CREATED = 'c';
const DATA_CHANGED = 'd';
const EVENT_REGEX = /^([cd]:)(.+)/;
const createCustomPostgreSqlPersister = (
  store,
  configOrStoreTableName,
  rawExecuteCommand,
  addChangeListener,
  delChangeListener,
  onSqlCommand,
  onIgnoredError,
  destroy,
  persist,
  thing,
  getThing = 'getDb',
) => {
  const executeCommand = getWrappedCommand(rawExecuteCommand, onSqlCommand);
  const [isJson, , defaultedConfig, managedTableNamesSet] = getConfigStructures(
    configOrStoreTableName,
  );
  const configHash =
    EMPTY_STRING + getHash(jsonStringWithUndefined(defaultedConfig));
  const channel = TINYBASE + '_' + configHash;
  const createFunction = async (
    name,
    body,
    returnPrefix = '',
    declarations = '',
  ) => {
    const escapedFunctionName = escapeIds(TINYBASE, name, configHash);
    await executeCommand(
      CREATE +
        OR_REPLACE +
        FUNCTION +
        escapedFunctionName +
        `()RETURNS ${returnPrefix}trigger AS $$ ${declarations}BEGIN ${body}END;$$ LANGUAGE plpgsql;`,
    );
    return escapedFunctionName;
  };
  const createTrigger = async (
    prefix,
    escapedTriggerName,
    body,
    escapedFunctionName,
  ) => {
    await executeCommand(
      CREATE +
        prefix +
        'TRIGGER' +
        escapedTriggerName +
        body +
        'EXECUTE ' +
        FUNCTION +
        escapedFunctionName +
        `()`,
    );
    return escapedTriggerName;
  };
  const notify = (message) => `PERFORM pg_notify('${channel}',${message});`;
  const when = (tableName, newOrOldOrBoth) =>
    isJson
      ? TRUE
      : newOrOldOrBoth === 2
        ? when(tableName, 0) + ' OR ' + when(tableName, 1)
        : strReplace(
            mapGet(defaultedConfig[0], tableName)?.[2] ?? TRUE,
            TABLE_NAME_PLACEHOLDER,
            newOrOldOrBoth == 0 ? 'NEW' : 'OLD',
          );
  const addDataChangedTriggers = (tableName, dataChangedFunction) =>
    promiseAll(
      arrayMap([INSERT, DELETE, UPDATE], (action, newOrOldOrBoth) =>
        createTrigger(
          OR_REPLACE,
          escapeIds(TINYBASE, DATA_CHANGED, configHash, tableName, action),
          `AFTER ${action} ON${escapeId(tableName)}FOR EACH ROW WHEN(${when(
            tableName,
            newOrOldOrBoth,
          )})`,
          dataChangedFunction,
        ),
      ),
    );
  const addPersisterListener = async (listener) => {
    const tableCreatedFunctionName = await createFunction(
      TABLE_CREATED,
      // eslint-disable-next-line max-len
      `FOR row IN SELECT object_identity FROM pg_event_trigger_ddl_commands()${WHERE} command_tag='${CREATE_TABLE}' LOOP ${notify(`'c:'||SPLIT_PART(row.object_identity,'.',2)`)}END LOOP;`,
      'event_',
      'DECLARE row record;',
    );
    await createTrigger(
      'EVENT ',
      escapeIds(TINYBASE, TABLE_CREATED, configHash),
      `ON ddl_command_end WHEN TAG IN('${CREATE_TABLE}')`,
      tableCreatedFunctionName,
    );
    const dataChangedFunctionName = await createFunction(
      DATA_CHANGED,
      notify(`'d:'||TG_TABLE_NAME`) + `RETURN NULL;`,
    );
    await promiseAll(
      arrayMap(collValues(managedTableNamesSet), async (tableName) => {
        await executeCommand(
          CREATE_TABLE +
            ` IF NOT EXISTS${escapeId(tableName)}("_id"text PRIMARY KEY)`,
        );
        return await addDataChangedTriggers(tableName, dataChangedFunctionName);
      }),
    );
    const listenerHandle = await addChangeListener(
      channel,
      (prefixAndTableName) =>
        ifNotUndefined(
          strMatch(prefixAndTableName, EVENT_REGEX),
          async ([, eventType, tableName]) => {
            if (collHas(managedTableNamesSet, tableName)) {
              if (eventType == 'c:') {
                await addDataChangedTriggers(
                  tableName,
                  dataChangedFunctionName,
                );
              }
              listener();
            }
          },
        ),
    );
    return [
      listenerHandle,
      [tableCreatedFunctionName, dataChangedFunctionName],
    ];
  };
  const delPersisterListener = async ([listenerHandle, functionNames]) => {
    delChangeListener(listenerHandle);
    await executeCommand(
      `DROP FUNCTION IF EXISTS${arrayJoin(functionNames, ',')}CASCADE`,
    );
  };
  return (isJson ? createJsonPersister : createTabularPersister)(
    store,
    executeCommand,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    destroy,
    persist,
    defaultedConfig,
    collValues(managedTableNamesSet),
    async (executeCommand2, managedTableNames) =>
      await executeCommand2(
        SELECT + // eslint-disable-next-line max-len
          ` table_name tn,column_name cn FROM information_schema.columns ${WHERE} table_schema='public'AND table_name IN(${getPlaceholders(managedTableNames)})`,
        managedTableNames,
      ),
    thing,
    getThing,
    'text',
    void 0,
    (cellOrValue) => jsonString(cellOrValue),
    (field) => jsonParse(field),
  );
};

const createCustomSqlitePersister = (
  store,
  configOrStoreTableName,
  rawExecuteCommand,
  addChangeListener,
  delChangeListener,
  onSqlCommand,
  onIgnoredError,
  destroy,
  persist,
  thing,
  getThing = 'getDb',
  upsert,
) => {
  let dataVersion;
  let schemaVersion;
  let totalChanges;
  const executeCommand = getWrappedCommand(rawExecuteCommand, onSqlCommand);
  const [
    isJson,
    autoLoadIntervalSeconds,
    defaultedConfig,
    managedTableNamesSet,
  ] = getConfigStructures(configOrStoreTableName);
  const addPersisterListener = (listener) => {
    let interval;
    const startPolling = () =>
      (interval = startInterval(
        () =>
          tryCatch(async () => {
            const [{d, s, c}] = await executeCommand(
              SELECT + // eslint-disable-next-line max-len
                ` ${DATA_VERSION} d,${SCHEMA_VERSION} s,TOTAL_CHANGES() c FROM ${PRAGMA}${DATA_VERSION} JOIN ${PRAGMA}${SCHEMA_VERSION}`,
            );
            if (d != dataVersion || s != schemaVersion || c != totalChanges) {
              if (!isNullish(dataVersion)) {
                listener();
              }
              dataVersion = d;
              schemaVersion = s;
              totalChanges = c;
            }
          }),
        autoLoadIntervalSeconds,
      ));
    const stopPolling = () => {
      dataVersion = schemaVersion = totalChanges = null;
      stopInterval(interval);
    };
    const listeningHandle = addChangeListener((tableName) => {
      if (managedTableNamesSet.has(tableName)) {
        stopPolling();
        listener();
        startPolling();
      }
    });
    startPolling();
    return () => {
      stopPolling();
      delChangeListener(listeningHandle);
    };
  };
  const delPersisterListener = (stopPollingAndDelUpdateListener) =>
    stopPollingAndDelUpdateListener();
  return (isJson ? createJsonPersister : createTabularPersister)(
    store,
    executeCommand,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    destroy,
    persist,
    defaultedConfig,
    collValues(managedTableNamesSet),
    async (executeCommand2, managedTableNames) =>
      await executeCommand2(
        SELECT + // eslint-disable-next-line max-len
          ` t.name tn,c.name cn FROM ${PRAGMA_TABLE}list()t,${PRAGMA_TABLE}info(t.name)c ${WHERE} t.schema='main'AND t.type IN('table','view')AND t.name IN(${getPlaceholders(managedTableNames)})ORDER BY t.name,c.name`,
        managedTableNames,
      ),
    thing,
    getThing,
    EMPTY_STRING,
    upsert,
    (cellOrValue) =>
      isTrue(cellOrValue) ? 1 : isFalse(cellOrValue) ? 0 : cellOrValue,
    void 0,
  );
};

export {
  Persists,
  Status,
  createCustomPersister,
  createCustomPostgreSqlPersister,
  createCustomSqlitePersister,
};
