From d998affe59003aaa95ff756d554aa7f81dc46d55 Mon Sep 17 00:00:00 2001 From: Alvaro Date: Mon, 20 Jul 2026 13:27:29 +0200 Subject: [PATCH] fix(pull): exclude ObsessionDB metadata tables from pulled schema chkit pull emitted ObsessionDB product-metadata tables (metadata_folder, metadata_table_folder, metadata_table_tag) as schema definitions. These are ObsessionDB internals provisioned inside customer databases, not the user's schema, so pulling them polluted the schema file and produced drift against tables the user does not own. They are now filtered before rendering on both the executor and custom- introspector paths, and excluded from the skipped-object summary (matching mapIntrospectedObjectToDefinition's rule that a kind-less object is a table). Closes #126 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Biy2vG7iixRiBsBBsFg5Nu --- .../pull-exclude-obsessiondb-metadata.md | 5 ++ packages/plugin-pull/src/index.test.ts | 64 +++++++++++++++++++ packages/plugin-pull/src/index.ts | 35 ++++++++-- 3 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 .changeset/pull-exclude-obsessiondb-metadata.md diff --git a/.changeset/pull-exclude-obsessiondb-metadata.md b/.changeset/pull-exclude-obsessiondb-metadata.md new file mode 100644 index 0000000..8b46bc7 --- /dev/null +++ b/.changeset/pull-exclude-obsessiondb-metadata.md @@ -0,0 +1,5 @@ +--- +"@chkit/plugin-pull": patch +--- + +`chkit pull` no longer emits ObsessionDB product-metadata tables (`metadata_folder`, `metadata_table_folder`, `metadata_table_tag`) into generated schema files. These are ObsessionDB internals provisioned inside customer databases, not part of the user's schema — emitting them polluted the schema and caused drift against tables the user does not own. They are now excluded before rendering and are not counted as skipped/unsupported objects. diff --git a/packages/plugin-pull/src/index.test.ts b/packages/plugin-pull/src/index.test.ts index 91ec3a8..ea02aaf 100644 --- a/packages/plugin-pull/src/index.test.ts +++ b/packages/plugin-pull/src/index.test.ts @@ -388,6 +388,70 @@ describe('@chkit/plugin-pull schema command', () => { expect(payload.content).toContain('default: "fn:\'\'"') }) + test('excludes ObsessionDB metadata tables from pulled schema output', async () => { + const introspectedTable = (name: string) => ({ + database: 'app', + name, + engine: 'MergeTree()', + primaryKey: '(id)', + orderBy: '(id)', + columns: [{ name: 'id', type: 'UInt64' }], + settings: {}, + indexes: [], + projections: [], + }) + const plugin = createPullPlugin({ + databases: ['app'], + introspect: async () => [ + introspectedTable('events'), + introspectedTable('metadata_folder'), + introspectedTable('metadata_table_folder'), + introspectedTable('metadata_table_tag'), + ], + }) + + const command = plugin.commands[0] + if (!command) throw new Error('missing command') + + const logs: unknown[] = [] + const code = await command.run({ + args: [], + flags: { '--dryrun': true }, + jsonMode: true, + options: PullSchema.parse({ databases: ['app'] }), + rawOptions: { databases: ['app'] }, + configPath: '/tmp/clickhouse.config.ts', + config: { + schema: ['./schema.ts'], + outDir: './chkit', + migrationsDir: './chkit/migrations', + metaDir: './chkit/meta', + plugins: [], + check: { failOnPending: true, failOnChecksumMismatch: true, failOnDrift: true }, + safety: { allowDestructive: false }, + clickhouse: { + url: 'http://localhost:8123', + username: 'default', + password: '', + database: 'default', + secure: false, + }, + }, + print(value) { + logs.push(value) + }, + }) + + expect(code).toBe(0) + const payload = logs[0] as { definitionCount: number; tableCount: number; content: string } + expect(payload.definitionCount).toBe(1) + expect(payload.tableCount).toBe(1) + expect(payload.content).toContain('const app_events = table({') + expect(payload.content).not.toContain('metadata_folder') + expect(payload.content).not.toContain('metadata_table_folder') + expect(payload.content).not.toContain('metadata_table_tag') + }) + test('supports introspected view and materialized_view objects', async () => { const plugin = createPullPlugin({ databases: ['app'], diff --git a/packages/plugin-pull/src/index.ts b/packages/plugin-pull/src/index.ts index dbb6be0..1064ae2 100644 --- a/packages/plugin-pull/src/index.ts +++ b/packages/plugin-pull/src/index.ts @@ -245,6 +245,23 @@ export function pull(options: PullPluginOptions = {}): PullPluginRegistration { // ───── Internal helpers ───── +// ObsessionDB provisions these product-metadata tables inside customer +// databases. They are ObsessionDB internals, not part of the user's schema, so +// pull must neither emit them nor count them as skipped/unsupported (#126). +const OBSESSIONDB_METADATA_TABLES = new Set([ + 'metadata_folder', + 'metadata_table_folder', + 'metadata_table_tag', +]) + +function isObsessionDbMetadataTable(object: { kind?: string; name: string }): boolean { + // An introspected object is a table when it says so or omits `kind` entirely, + // mirroring mapIntrospectedObjectToDefinition — a custom introspector returns + // bare table objects without a `kind` field. + const isTable = object.kind == null || object.kind === 'table' + return isTable && OBSESSIONDB_METADATA_TABLES.has(object.name) +} + async function pullSchema(input: { config: ResolvedChxConfig executor?: ClickHouseExecutor | null @@ -274,18 +291,22 @@ async function pullSchema(input: { let selectedDatabases = input.options.databases if (db && (!customIntrospector || selectedDatabases.length === 0)) { - objects = await db.listSchemaObjects() + objects = (await db.listSchemaObjects()).filter( + (item) => !isObsessionDbMetadataTable(item) + ) if (selectedDatabases.length === 0) { selectedDatabases = [...new Set(objects.map((item) => item.database))].sort() } } - const introspected = customIntrospector - ? await customIntrospector({ - config: input.config.clickhouse as NonNullable, - databases: selectedDatabases, - }) - : await introspectWithExecutor(db as ClickHouseExecutor, selectedDatabases) + const introspected = ( + customIntrospector + ? await customIntrospector({ + config: input.config.clickhouse as NonNullable, + databases: selectedDatabases, + }) + : await introspectWithExecutor(db as ClickHouseExecutor, selectedDatabases) + ).filter((object) => !isObsessionDbMetadataTable(object)) const definitions = canonicalizeDefinitions(introspected.map(mapIntrospectedObjectToDefinition)) const content = renderSchemaFile(definitions)