AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: add isJson flag to ColumnDef for dialect-agnostic JSON column detection

SQLite maps JSON columns to TEXT, so checking col.sqlType === 'JSON'
only worked for DuckDB. This caused reshapeRow and insert operations
to skip JSON parsing/serialization on SQLite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+37 -29
+5 -5
packages/hatk/src/database/db.ts
··· 574 574 575 575 if (rawValue === undefined || rawValue === null) { 576 576 values.push(null) 577 - } else if (col.sqlType === 'JSON') { 577 + } else if (col.isJson) { 578 578 values.push(JSON.stringify(rawValue)) 579 579 } else { 580 580 values.push(rawValue) ··· 617 617 const raw = item[col.originalName] 618 618 if (raw === undefined || raw === null) { 619 619 values.push(null) 620 - } else if (col.sqlType === 'JSON') { 620 + } else if (col.isJson) { 621 621 values.push(JSON.stringify(raw)) 622 622 } else { 623 623 values.push(raw) ··· 656 656 const raw = item[col.originalName] 657 657 if (raw === undefined || raw === null) { 658 658 values.push(null) 659 - } else if (col.sqlType === 'JSON') { 659 + } else if (col.isJson) { 660 660 values.push(JSON.stringify(raw)) 661 661 } else { 662 662 values.push(raw) ··· 680 680 const raw = branchData[col.originalName] 681 681 if (raw === undefined || raw === null) { 682 682 values.push(null) 683 - } else if (col.sqlType === 'JSON') { 683 + } else if (col.isJson) { 684 684 values.push(JSON.stringify(raw)) 685 685 } else { 686 686 values.push(raw) ··· 1496 1496 if (schema) { 1497 1497 for (const col of schema.columns) { 1498 1498 nameMap.set(col.name, col.originalName) 1499 - if (col.sqlType === 'JSON') jsonCols.add(col.name) 1499 + if (col.isJson) jsonCols.add(col.name) 1500 1500 } 1501 1501 } 1502 1502
+1 -1
packages/hatk/src/database/fts.ts
··· 130 130 if (col.sqlType === 'TEXT') { 131 131 selectExprs.push(`t.${col.name}`) 132 132 searchColNames.push(col.name) 133 - } else if ((col.sqlType === 'JSON' || col.sqlType === 'TEXT') && record?.properties) { 133 + } else if (col.isJson && record?.properties) { 134 134 const prop = record.properties[col.originalName] 135 135 if (prop?.type === 'blob') continue // skip blobs 136 136 if (prop && lexicon) {
+30 -22
packages/hatk/src/database/schema.ts
··· 6 6 export interface ColumnDef { 7 7 name: string // snake_case column name 8 8 originalName: string // camelCase lexicon field name 9 - sqlType: string // DuckDB type 9 + sqlType: string // dialect-specific SQL type (e.g. 'JSON' for DuckDB, 'TEXT' for SQLite) 10 10 notNull: boolean 11 11 isRef: boolean // true if this column holds an AT URI referencing another record 12 + isJson: boolean // true if this column stores JSON data (blobs, refs, arrays, objects, unions) 12 13 } 13 14 14 15 export interface UnionBranchSchema { ··· 51 52 interface TypeMapping { 52 53 sqlType: string 53 54 isRef: boolean 55 + isJson: boolean 54 56 } 55 57 56 58 function mapType(prop: any, dialect: SqlDialect): TypeMapping { 57 59 if (prop.type === 'string') { 58 - if (prop.format === 'datetime') return { sqlType: dialect.typeMap.timestamp, isRef: false } 59 - if (prop.format === 'at-uri') return { sqlType: dialect.typeMap.text, isRef: true } 60 - return { sqlType: dialect.typeMap.text, isRef: false } 60 + if (prop.format === 'datetime') return { sqlType: dialect.typeMap.timestamp, isRef: false, isJson: false } 61 + if (prop.format === 'at-uri') return { sqlType: dialect.typeMap.text, isRef: true, isJson: false } 62 + return { sqlType: dialect.typeMap.text, isRef: false, isJson: false } 61 63 } 62 - if (prop.type === 'integer') return { sqlType: dialect.typeMap.integer, isRef: false } 63 - if (prop.type === 'boolean') return { sqlType: dialect.typeMap.boolean, isRef: false } 64 - if (prop.type === 'bytes') return { sqlType: dialect.typeMap.blob, isRef: false } 65 - if (prop.type === 'cid-link') return { sqlType: dialect.typeMap.text, isRef: false } 66 - if (prop.type === 'array') return { sqlType: dialect.jsonType, isRef: false } 67 - if (prop.type === 'blob') return { sqlType: dialect.jsonType, isRef: false } 68 - if (prop.type === 'union') return { sqlType: dialect.jsonType, isRef: false } 69 - if (prop.type === 'unknown') return { sqlType: dialect.jsonType, isRef: false } 70 - if (prop.type === 'object') return { sqlType: dialect.jsonType, isRef: false } 64 + if (prop.type === 'integer') return { sqlType: dialect.typeMap.integer, isRef: false, isJson: false } 65 + if (prop.type === 'boolean') return { sqlType: dialect.typeMap.boolean, isRef: false, isJson: false } 66 + if (prop.type === 'bytes') return { sqlType: dialect.typeMap.blob, isRef: false, isJson: false } 67 + if (prop.type === 'cid-link') return { sqlType: dialect.typeMap.text, isRef: false, isJson: false } 68 + if (prop.type === 'array') return { sqlType: dialect.jsonType, isRef: false, isJson: true } 69 + if (prop.type === 'blob') return { sqlType: dialect.jsonType, isRef: false, isJson: true } 70 + if (prop.type === 'union') return { sqlType: dialect.jsonType, isRef: false, isJson: true } 71 + if (prop.type === 'unknown') return { sqlType: dialect.jsonType, isRef: false, isJson: true } 72 + if (prop.type === 'object') return { sqlType: dialect.jsonType, isRef: false, isJson: true } 71 73 if (prop.type === 'ref') { 72 74 // strongRef contains { uri, cid } — handled specially in generateTableSchema 73 - if (prop.ref === 'com.atproto.repo.strongRef') return { sqlType: 'STRONG_REF', isRef: true } 74 - return { sqlType: dialect.jsonType, isRef: false } 75 + if (prop.ref === 'com.atproto.repo.strongRef') return { sqlType: 'STRONG_REF', isRef: true, isJson: false } 76 + return { sqlType: dialect.jsonType, isRef: false, isJson: true } 75 77 } 76 - return { sqlType: dialect.typeMap.text, isRef: false } 78 + return { sqlType: dialect.typeMap.text, isRef: false, isJson: false } 77 79 } 78 80 79 81 // Recursively find all .json files in a directory ··· 239 241 240 242 const columns: ColumnDef[] = [] 241 243 for (const [propName, prop] of Object.entries(propSource)) { 242 - const { sqlType, isRef } = mapType(prop as any, dialect) 244 + const { sqlType, isRef, isJson } = mapType(prop as any, dialect) 243 245 // Skip STRONG_REF expansion in branch tables — treat as JSON 244 246 const finalType = sqlType === 'STRONG_REF' ? dialect.jsonType : sqlType 245 247 columns.push({ ··· 247 249 originalName: propName, 248 250 sqlType: finalType, 249 251 notNull: branchRequired.has(propName), 250 - isRef: finalType !== 'JSON' && isRef, 252 + isRef: finalType !== dialect.jsonType && isRef, 253 + isJson: isJson || sqlType === 'STRONG_REF', 251 254 }) 252 255 } 253 256 ··· 296 299 sqlType: dialect.jsonType, 297 300 notNull: required.has(fieldName), 298 301 isRef: false, 302 + isJson: true, 299 303 }) 300 304 continue 301 305 } ··· 307 311 const childColumns: ColumnDef[] = [] 308 312 const itemRequired = new Set(p.items?.required || lexicon.defs?.[p.items?.ref?.slice(1)]?.required || []) 309 313 for (const [itemField, itemProp] of Object.entries(itemProps)) { 310 - const { sqlType, isRef } = mapType(itemProp as any, dialect) 314 + const { sqlType, isRef, isJson } = mapType(itemProp as any, dialect) 311 315 childColumns.push({ 312 316 name: toSnakeCase(itemField), 313 317 originalName: itemField, 314 318 sqlType, 315 319 notNull: itemRequired.has(itemField), 316 320 isRef, 321 + isJson, 317 322 }) 318 323 } 319 324 const snakeField = toSnakeCase(fieldName) ··· 327 332 } 328 333 } 329 334 330 - const { sqlType, isRef } = mapType(p, dialect) 335 + const { sqlType, isRef, isJson } = mapType(p, dialect) 331 336 332 337 if (sqlType === 'STRONG_REF') { 333 338 // Expand strongRef into two columns: {name}_uri and {name}_cid ··· 337 342 sqlType: dialect.typeMap.text, 338 343 notNull: required.has(fieldName), 339 344 isRef: true, 345 + isJson: false, 340 346 }) 341 347 columns.push({ 342 348 name: toSnakeCase(fieldName) + '_cid', ··· 344 350 sqlType: dialect.typeMap.text, 345 351 notNull: required.has(fieldName), 346 352 isRef: false, 353 + isJson: false, 347 354 }) 348 355 } else { 349 356 columns.push({ ··· 352 359 sqlType, 353 360 notNull: required.has(fieldName), 354 361 isRef, 362 + isJson, 355 363 }) 356 364 } 357 365 } ··· 410 418 childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_did ON ${child.tableName}(parent_did);`) 411 419 412 420 for (const col of child.columns) { 413 - if (col.sqlType === 'JSON' || col.sqlType === 'BLOB') continue 421 + if (col.isJson || col.sqlType === 'BLOB') continue 414 422 childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_${col.name} ON ${child.tableName}(${col.name});`) 415 423 } 416 424 } ··· 430 438 childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_did ON ${branch.tableName}(parent_did);`) 431 439 432 440 for (const col of branch.columns) { 433 - if (col.sqlType === 'JSON' || col.sqlType === 'BLOB') continue 441 + if (col.isJson || col.sqlType === 'BLOB') continue 434 442 childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_${col.name} ON ${branch.tableName}(${col.name});`) 435 443 } 436 444 }
+1 -1
packages/hatk/src/labels.ts
··· 185 185 for (const col of schema.columns) { 186 186 let v = row[col.name] 187 187 if (v === null || v === undefined) continue 188 - if (col.sqlType === 'JSON' && typeof v === 'string') { 188 + if (col.isJson && typeof v === 'string') { 189 189 try { 190 190 v = JSON.parse(v) 191 191 } catch {}