Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
420fb8c
wip(plugin-webhooks): declared webhooks read from the registry; seede…
claude Oct 19, 2026
e44c642
test(plugin-webhooks): pin the registry read, the ledger switch and t…
claude Oct 19, 2026
8189a84
wip: liveness ledger re-anchored to the registry read; dogfood proof …
claude Oct 19, 2026
e412cb8
docs+ledger: the registry read named across spec comments, liveness, …
claude Oct 19, 2026
bdad70b
spec(liveness): state counts regenerated; isActive named at the dispa…
claude Oct 19, 2026
723af97
fix(plugin-webhooks): a declared webhook is deployment-wide when the …
claude Oct 19, 2026
e0a6dc9
fix(gates): tenant-audit census regenerated for the seeder's two reti…
claude Oct 19, 2026
6b3042a
test(plugin-webhooks): the fake engine refuses a WHERE combinator it …
claude Oct 19, 2026
820121c
fix(lint,showcase): the dead label/description verdict is carried to …
claude Oct 19, 2026
b378ff5
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
c3b1452
test(dogfood): the R2 harness pins one delivery per task change from …
claude Oct 19, 2026
9bce0aa
test(dogfood): the harness case switches the declaration on, so it ca…
claude Oct 19, 2026
7e580dc
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
fa6a837
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
b4ae8e6
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
206d2bc
Merge remote-tracking branch 'origin/claude/issue-24529-8b0g-webhooks…
claude Oct 19, 2026
f89c0f2
wip: birth marker, shapeGeneration, boot guard rule
claude Oct 19, 2026
6e14afa
wip: ceremony steps 5a-5d, inventory, flips, differ ownership
claude Oct 19, 2026
50a2d34
wip: open the ceremony command, ports for webhooks and NOT NULL
claude Oct 19, 2026
9ea4312
wip: existing ceremony tests follow the new step set; NOT NULL routed…
claude Oct 19, 2026
719ab13
wip: the guard's pins follow the birth marker and shapeGeneration
claude Oct 19, 2026
036a2a1
wip: the birth half in every CLI boot, so a CLI-created fresh store b…
claude Oct 19, 2026
0128f07
test(cli): pin the generation-1 ceremony steps over an 18.0-shaped da…
claude Oct 19, 2026
8e6a33a
test: the tenant column's NOT NULL is the ceremony's, not drift; the …
claude Oct 19, 2026
32d1e22
test(dogfood): R2 and R3 restated — the 18.0 and v17 databases take t…
claude Oct 19, 2026
13446cb
fix(cli): a rotation shard's NOT NULL is reported unsupported, never …
claude Oct 19, 2026
06e0556
docs,qa: the open ceremony and the birth marker in the CLI docs and t…
claude Oct 19, 2026
e0c1d2b
chore(spec): regenerate the api-surface, export-origin, declaration-m…
claude Oct 19, 2026
a0fa816
test(platform-objects,service-settings): sys_secret is deployment-lev…
claude Oct 19, 2026
e7a788d
chore: regenerate the platform-object tenancy census (sys_secret and …
claude Oct 19, 2026
f1264d7
chore: the gates the sweep named — the guard double's findOne predica…
claude Oct 19, 2026
a3064ef
test(cli): fixtures standing in for an earlier release's database car…
claude Oct 19, 2026
d6195b9
Merge origin/main into claude/issue-24581-ceremony-open
claude Oct 19, 2026
0232108
docs(qa): the ceremony item's revision 3 cites its PR
claude Oct 19, 2026
dceaa56
fix(driver-turso): the remote face counts what its schema doors creat…
claude Oct 19, 2026
33b7943
test(runtime): the remote-face guard pin reads driver-turso's source
claude Oct 19, 2026
d28f4cf
chore(changeset): the remote face's schema-sync count
claude Oct 19, 2026
edb2db1
test(dogfood): the upgrade ceremony over the 18.0 fixture is a shared…
claude Oct 19, 2026
1699ec5
Merge origin/main after #24667 (step 4) into the ceremony branch
claude Oct 20, 2026
b5010d9
test(dogfood): ceremoniedFixture boots the 18.0 bytes once, refused, …
claude Oct 20, 2026
0f83a8d
fix(text): no shipped text says the ownership ceremony is closed; an …
claude Oct 20, 2026
0ba98c1
Merge remote-tracking branch 'origin/claude/issue-24581-ceremony-open…
claude Oct 20, 2026
4b4e06d
wip(7b-3): refuse a production server boot whose auth did not compose…
claude Oct 20, 2026
9bbd092
wip(7b-3): retire the organization-less counter's texts
claude Oct 20, 2026
d6d7526
wip(7b-3): pins for the counter refusal and the no-secret serve refusal
claude Oct 20, 2026
fc9f14d
wip(7b-3): the refusal pin runs per PR; the degraded-banner pin boots…
claude Oct 20, 2026
b4ef00a
test(7b-3): pins of the retired organization-less counter land their …
claude Oct 20, 2026
2aa3b78
chore(7b-3): changeset
claude Oct 20, 2026
b2fec9a
test(7b-3): the refusal and explanation pins assert the new rule, not…
claude Oct 20, 2026
97fbc9c
test(7b-3): production server rigs whose property is not auth hand in…
claude Oct 20, 2026
4bb5ed8
chore(7b-3): the refusal pin joins the built-entrypoint census
claude Oct 20, 2026
112ea87
Merge remote-tracking branch 'origin/main' into claude/issue-24583-7b…
claude Oct 20, 2026
ef624ed
Merge remote-tracking branch 'origin/main' into claude/issue-24583-7b…
claude Oct 20, 2026
82c14dd
fix(7b-3): the changeset names the retired export in ADR-0087's runti…
claude Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/24583-7b3-organizationless-counter-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@objectstack/cli': minor
'@objectstack/driver-sql': minor
'@objectstack/driver-turso': minor
'@objectstack/objectql': minor
'@objectstack/lint': patch
'@objectstack/spec': patch
---

feat!: an organization-less row on a tenant table no longer draws its autonumber from the `__global__` counter, and a production server boot with no auth secret is refused

Clause-②: yes (narrowing)

<!-- adr-0087: not-required (runtime-export-retired @objectstack/objectql#GLOBAL_TENANT) The one author-reachable removal is a runtime export: the string constant `GLOBAL_TENANT` leaves `@objectstack/objectql`'s published entry. No key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone; the two run-time narrowings (the driver's counter, `os serve`'s boot) carry their own remedy below. -->

**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D9, stage 8d).

- **`os serve`: a production server boot whose auth did not compose for a missing secret is refused.** With the `auth` tier on and no `OS_AUTH_SECRET` (outside `--dev`), `serve` used to skip AuthPlugin with a warning and serve anyway. That boot had no security model: anonymous object data is always denied, so its data API was unusable. It also had no organization object, so its inline seed wrote rows with `organization_id` NULL and drew their autonumbers from the `__global__` counter. Measured on `examples/app-showcase` before this change: health 200, two NULL-organization rows and a `__global__` counter row on a fresh install. The boot now stops before the kernel starts, so nothing is written, and the message names both fixes. Unchanged: `--no-server` (kernel only) still boots with the warning, a development boot still uses its fallback secret, and the cloud host kernel (`ObjectOSEnvironmentPlugin`, where auth is each project's) still only warns. `os start` never reaches the refusal: it always supplies a secret to the `serve` child it spawns.
- **`@objectstack/driver-sql` (and `@objectstack/driver-turso`'s remote face, which calls the same rule): the counter refuses an organization-less row on a tenant table.** Wherever the driver registered the organization object (`sys_organization`), a `create` / `bulkCreate` / `upsert` on an object with a tenant column, whose row carries no organization and whose options carry no `tenantId`, is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (status 500), the code the engine already answers for the same write. It is refused before any counter row is read or written, and nothing is written. New exports: `OrganizationlessCounterRefusal`, `ORGANIZATIONLESS_COUNTER_REFUSAL_CODE`, `ORGANIZATIONLESS_COUNTER_REFUSAL_STATUS`, `renderOrganizationlessCounterRefusal`. Unchanged: `__global__` stays the counter key of an object with no tenant column, and of a composition that registers no organization object on the driver (a lean `@objectstack/objectql/core` embedding), where no organization can exist. Counter rows a 17.x install filed under `__global__` for a tenant table stay in `_objectstack_sequences`; the counter no longer reads them, and `os migrate duplicates` still reports them.
- **`@objectstack/objectql`: `GLOBAL_TENANT` is no longer exported**, and `SystemWriteOrganizationRequiredError`'s message no longer describes the `__global__` counter as where the write would land. The code, status and fields are unchanged.
- **Texts.** The lint explanation of `unique/legacy-organization-composite`, the `UniqueScopeSchema` TSDoc and the error-code ledger now say that a NULL-organization row is residue an earlier release wrote, not something a new write produces.

**The remedy.**
- A production boot refused for the missing secret: set `OS_AUTH_SECRET` (a long random value, identical across restarts and nodes), or run kernel-only with `--no-server`.
- A direct driver write refused by the counter: carry the organization — `driver.create(object, { ...row, organization_id: orgId })` or `driver.create(object, row, { tenantId: orgId })` — or write through the engine, which derives it under `single`.

The retired export:

- FROM `GLOBAL_TENANT` TO `GLOBAL_TENANT` imported from `@objectstack/driver-sql`, where the counter and the index emitters keep it (or the literal `'__global__'`).
18 changes: 13 additions & 5 deletions packages/cli/src/commands/migrate/duplicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ import type { RuntimeIndexPreflight, SeedTenancyExec } from '@objectstack/metada
* schema, no rows written, nothing left behind.
* 4. **The live CONDITION is reported too** — an install still holding a
* `__global__` counter beside an organization-scoped one for the same
* object is about to mint more duplicates. The only forward-looking line in
* the report.
* object. Under 17.x that install was about to mint more duplicates, and
* this was the only forward-looking line in the report. Since ADR-0131 D9
* (stage 8d) the counter refuses an organization-less row on a tenant
* table wherever the organization object is registered, so such a
* `__global__` row is no longer drawn from: the line now names the residue
* the split left in `_objectstack_sequences` — the evidence of which
* counter minted the duplicates above — rather than a forecast. The report
* shape is unchanged.
* 5. **A data-side probe** — `GROUP BY <field> HAVING COUNT(*) > 1` within the
* object's own table, never an enumeration of `_objectstack_sequences`. A
* duplicate on an object whose counter was since merged is therefore still
Expand Down Expand Up @@ -154,10 +160,12 @@ export interface DuplicateIdentifier {
}

/**
* One object/field still running two counters — the live condition (point 4).
* One object/field still holding two counters — the live condition (point 4).
*
* Not damage: a prediction. While both counters stand, each allocates within
* its own partition and neither can see the other's numbers.
* Not damage. Under 17.x it was a prediction: while both counters stood, each
* allocated within its own partition and neither could see the other's
* numbers. Since ADR-0131 D9 the `__global__` one is residue the counter no
* longer draws from on a tenant table (point 4 above).
*/
export interface DuplicateLiveCondition {
object: string;
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4674,6 +4674,29 @@ export default class Serve extends Command {
+ "view, a share link, or `book.audience: 'public'`.",
);
}
// [ADR-0131 D9] The same case reached the other way: the `auth` tier is
// on, but a production boot resolved no secret, so AuthPlugin is not
// composed (rule 4 of `resolvePlatformAuthComposition`). A1's conclusion
// holds unchanged — no security model, so no data API — and it holds for
// a second reason too: with no AuthPlugin there is no organization
// object, so every write the boot makes (the inline seed first) would land
// with no organization and draw its autonumbers from an organization-less
// counter. Refused here, while the kernel has only been composed: no
// plugin has started, so nothing has been written. The `host-kernel`
// reason (the cloud runtime, where auth is each project's) is not this
// case and still only warns above.
if (flags.server && !authComposition.composes && authComposition.reason === 'no-secret') {
throw new Error(
'This stack enables auth, but no auth secret is set, so AuthPlugin was not composed and no caller '
+ 'can authenticate — anonymous access to object data is always denied, so the data API would be '
+ 'unusable, and with no organization every row this boot writes would have no owner.\n'
+ 'Fix it one of two ways:\n'
+ ' • set OS_AUTH_SECRET (a long random value, identical across restarts and nodes), e.g. '
+ 'OS_AUTH_SECRET=$(openssl rand -hex 32);\n'
+ ' • or serve without the data API — run with --no-server, or drop the REST/dispatcher plugins.\n'
+ 'A local trial does not need the secret: `os dev` (or `os serve --dev`) uses a development fallback.',
);
}

try {
const { createRestApiPlugin } = await import('@objectstack/rest');
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/test/helpers/serve-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,16 @@ export function portDriftError(
*/
export const E2E_SECRET_KEY = '0e2e'.repeat(16);

/**
* A fixed, obviously-synthetic auth secret for spawned PRODUCTION server boots
* (`bin/run.js`, `NODE_ENV` unset). Since ADR-0131 D9 such a boot is refused
* before the kernel starts when no secret composes AuthPlugin
* (`serve-no-auth-secret-refusal.integration.test.ts`), so a rig whose
* property is not about auth hands this in rather than steering around the
* refusal. ⛔ Test fixtures only; it signs nothing anyone keeps.
*/
export const E2E_AUTH_SECRET = 'os-e2e-auth-secret-not-a-real-credential-0e2e0e2e';

export const VITEST_WORKER_ENV_KEYS = [
'TEST',
'VITEST',
Expand Down
137 changes: 137 additions & 0 deletions packages/cli/test/serve-no-auth-secret-refusal.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { spawn, type ChildProcessByStdio } from 'node:child_process';
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import type { Readable } from 'node:stream';
import { fileURLToPath } from 'node:url';
import {
childEnv,
E2E_SECRET_KEY,
KERNEL_ASSIGNED_PORT,
requireBuiltCli,
RUN_JS_RESOLVES_FROM_DIST,
} from './helpers/serve-process.js';

/**
* ADR-0131 D9 — a production `os serve` whose `auth` tier is on but whose auth
* secret is missing is REFUSED before the kernel starts.
*
* Without the secret `resolvePlatformAuthComposition` composes no AuthPlugin,
* so the boot has no security model (anonymous object data is always denied,
* the data API is unusable) and no organization object — and every row its
* inline seed writes on a tenant table landed with no organization, its
* autonumber drawn from the organization-less counter. Measured before this
* refusal on `examples/app-showcase`: a fresh install answered health 200 and
* held two rows with a NULL `organization_id` and a `__global__` counter row.
*
* The property pinned is the order as much as the refusal: it fires while the
* kernel is only composed, so the database is never even opened. The healthy
* leg is the control — the same fixture, entrypoint and posture with the
* secret set reaches the ready banner and creates the database file, which is
* what makes the absent file in the refused leg mean something.
*
* Spawned through `bin/run.js` with `NODE_ENV` unset, the only shape that
* reaches production posture (`bin/run-dev.js` sets `development`, and a
* development boot falls back to a local secret).
*/

const READY_BANNER_TAIL = /Press Ctrl\+C to stop/;
const HERE = resolve(fileURLToPath(import.meta.url), '..');
const CLI = resolve(HERE, '../bin/run.js');

type BootChild = ChildProcessByStdio<null, Readable, Readable>;

const children: BootChild[] = [];
let fixtureDir: string;

interface Outcome {
output: string;
exitCode: number | null;
}

/** Boot until the ready banner (returns `exitCode: null`) or until the process exits. */
async function boot(databaseFile: string, env: Record<string, string | undefined> = {}): Promise<Outcome> {
const child = spawn(process.execPath, [CLI, 'serve', 'objectstack.config.ts', '--port', KERNEL_ASSIGNED_PORT], {
cwd: fixtureDir,
stdio: ['ignore', 'pipe', 'pipe'],
// `childEnv`, never a bare `...process.env` — see its header (commit 1ddda1d00).
env: childEnv({
NO_COLOR: '1',
OS_DATABASE_URL: `file:${databaseFile}`,
OS_LOG_LEVEL: '',
OS_DISABLE_CONSOLE: '1',
OS_SECRET_KEY: E2E_SECRET_KEY,
// Truly unset in both legs unless a leg sets it: the refusal's subject.
// The two legacy spellings `resolveAuthSecret` still reads are unset too,
// so an ambient one cannot compose auth behind the leg's back.
OS_AUTH_SECRET: undefined,
AUTH_SECRET: undefined,
BETTER_AUTH_SECRET: undefined,
// Production posture — see the header.
NODE_ENV: undefined,
...env,
}),
}) as BootChild;
children.push(child);

let output = '';
return new Promise<Outcome>((done, fail) => {
const timer = setTimeout(() => {
fail(new Error(`serve neither refused nor reached ready\n${output}`));
}, 150_000);
const onData = (d: unknown) => {
output += String(d);
if (READY_BANNER_TAIL.test(output)) {
clearTimeout(timer);
done({ output, exitCode: null });
}
};
child.stdout.on('data', onData);
child.stderr.on('data', onData);
child.on('exit', (code) => {
clearTimeout(timer);
done({ output, exitCode: code });
});
});
}

beforeAll(() => {
requireBuiltCli(RUN_JS_RESOLVES_FROM_DIST);
fixtureDir = mkdtempSync(join(tmpdir(), 'os-no-auth-secret-'));
writeFileSync(join(fixtureDir, 'objectstack.config.ts'), 'export default {};\n', 'utf8');
});

afterAll(() => {
for (const child of children) {
try { child.kill('SIGTERM'); } catch { /* already gone */ }
}
if (fixtureDir) rmSync(fixtureDir, { recursive: true, force: true });
});

describe('os serve — a production server boot with no auth secret is refused (ADR-0131 D9)', () => {
it('refuses before the kernel starts: non-zero exit, the remedy named, the database never opened', async () => {
const databaseFile = join(fixtureDir, 'refused.db');
const { output, exitCode } = await boot(databaseFile);

expect(exitCode, `the boot was not refused\n${output}`).not.toBeNull();
expect(exitCode).not.toBe(0);
expect(output).not.toMatch(READY_BANNER_TAIL);
expect(output).toContain('no auth secret is set');
expect(output).toContain('OS_AUTH_SECRET');
expect(existsSync(databaseFile), `the refused boot opened its database\n${output}`).toBe(false);
}, 240_000);

it('control: the same boot with the secret reaches ready and opens its database', async () => {
const databaseFile = join(fixtureDir, 'healthy.db');
const { output, exitCode } = await boot(databaseFile, {
OS_AUTH_SECRET: 'os-adr-0131-d9-control-leg-secret-not-a-real-credential',
});

expect(exitCode, `the control leg exited\n${output}`).toBeNull();
expect(output).not.toContain('no auth secret is set');
expect(existsSync(databaseFile), `the control leg never opened its database\n${output}`).toBe(true);
}, 240_000);
});
8 changes: 7 additions & 1 deletion packages/cli/test/serve-ready-degraded-boot.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ import {
* incidents reached by other routes: a plugin that did not load, and a core
* service that is not there.
*
* [ADR-0131 D9] Both legs boot `--no-server`. A production SERVER boot with no
* secret is now refused before the kernel starts
* (`serve-no-auth-secret-refusal.integration.test.ts`), so it reaches no banner; the
* kernel-only boot still skips auth with the warning and still prints the
* ready banner, which is the property under test here.
*
* ⛔ `runServe()` cannot reach it. That helper spawns `bin/run-dev.js`, which
* sets `NODE_ENV = 'development'` before argv is parsed, so `isDev` is true,
* the dev fallback secret applies, auth ALWAYS loads and the boot is never
Expand Down Expand Up @@ -92,7 +98,7 @@ async function boot(env: Record<string, string | undefined> = {}): Promise<Boot>
// A kernel-assigned port (#24295): production posture refuses to auto-shift,
// so a port drawn and released beforehand is a red whenever another process
// takes it in the gap. Nothing here talks to the port.
const child = spawn(process.execPath, [CLI, 'serve', 'objectstack.config.ts', '--port', KERNEL_ASSIGNED_PORT], {
const child = spawn(process.execPath, [CLI, 'serve', 'objectstack.config.ts', '--no-server', '--port', KERNEL_ASSIGNED_PORT], {
cwd: fixtureDir,
stdio: ['ignore', 'pipe', 'pipe'],
// `childEnv`, never a bare `...process.env` — see its header (commit 1ddda1d00).
Expand Down
12 changes: 10 additions & 2 deletions packages/cli/test/uncreatable-directory-doors.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync,
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { E2E_SECRET_KEY, RUN_JS_RESOLVES_FROM_DIST, requireBuiltCli } from './helpers/serve-process.js';
import { E2E_AUTH_SECRET, E2E_SECRET_KEY, RUN_JS_RESOLVES_FROM_DIST, requireBuiltCli } from './helpers/serve-process.js';

const HERE = resolve(fileURLToPath(import.meta.url), '..');
const RUN_JS = resolve(HERE, '../bin/run.js');
Expand Down Expand Up @@ -171,7 +171,15 @@ function runCli(args: string[], env: Record<string, string>, opts: { cwd?: strin
function runServe(home: string): Promise<Run> {
return runCli(
['serve', '--port', '0'],
{ OS_HOME: home, OS_DATABASE_URL: ':memory:', OS_SECRET_KEY: E2E_SECRET_KEY, OS_DISABLE_CONSOLE: '1' },
{
OS_HOME: home,
OS_DATABASE_URL: ':memory:',
OS_SECRET_KEY: E2E_SECRET_KEY,
// A production server boot with no auth secret is refused (ADR-0131 D9);
// this file's property is about OS_HOME, not auth.
OS_AUTH_SECRET: E2E_AUTH_SECRET,
OS_DISABLE_CONSOLE: '1',
},
{ cwd: serveDir, until: BANNER_TAIL },
);
}
Expand Down
8 changes: 8 additions & 0 deletions packages/drivers/driver-sql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ export {
UnsupportedDialectEmissionError,
renderDialectEmissionRefusal,
} from './dialect-emission-refusal.js';
// [ADR-0131 D9] The autonumber counter's refusal of an organization-less row on
// a tenant table — exported for the same reason: a caller recognises it by `code`.
export {
ORGANIZATIONLESS_COUNTER_REFUSAL_CODE,
ORGANIZATIONLESS_COUNTER_REFUSAL_STATUS,
OrganizationlessCounterRefusal,
renderOrganizationlessCounterRefusal,
} from './organizationless-counter-refusal.js';
export type {
SqlDriverConfig,
SqliteJournalMode,
Expand Down
Loading
Loading