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)