···33BINARY_NAME=tumble
44BUILD_DIR=bin
5566-.PHONY: all build clean test deps docs kill restart reset-db load-fixtures build-linux help fmt
66+.PHONY: all build clean test test-mysql deps docs kill restart reset-db load-fixtures build-linux help fmt
7788all: build ## Build the binary (default)
99···33333434test: ## Run unit tests
3535 go test -v ./...
3636+3737+test-mysql: ## Run MySQL-specific tests (requires MySQL)
3838+ go test -v -tags mysql ./internal/data/
36393740test-api: build ## Run API tests
3841 ./tests/run_integration_tests.sh
···116116 // Exclude links with cached error previews using tiered TTLs:
117117 // - Recent links (< 10 days old): error cache expires after 24h
118118 // - Old links (>= 10 days old): error cache expires after 60 days
119119- // CAST(data AS TEXT) is required because glebarez/sqlite stores []byte
120120- // as BLOB, and SQLite's LIKE doesn't match text patterns against BLOBs.
119119+ // CAST is required because glebarez/sqlite stores []byte as BLOB,
120120+ // and SQLite's LIKE doesn't match text patterns against BLOBs.
121121+ // MySQL doesn't support CAST(... AS TEXT), so use CHAR instead.
122122+ castType := "TEXT"
123123+ if s.db.Dialector.Name() == "mysql" {
124124+ castType = "CHAR"
125125+ }
121126 recentCutoff := time.Now().Add(-24 * time.Hour)
122127 oldCutoff := time.Now().Add(-60 * 24 * time.Hour)
123128 linkAgeCutoff := time.Now().Add(-10 * 24 * time.Hour)
···125130 Where(`(title LIKE ? OR url LIKE ? OR ircLinkID IN (SELECT resource_id FROM tags WHERE resource_type = 'link' AND tag LIKE ?))
126131AND url NOT IN (
127132 SELECT lp.url FROM link_previews lp
128128- WHERE CAST(lp.data AS TEXT) LIKE '%"error":%'
133133+ WHERE CAST(lp.data AS `+castType+`) LIKE '%"error":%'
129134 AND (
130135 (EXISTS (SELECT 1 FROM ircLink il WHERE il.url = lp.url AND il.timestamp > ?) AND lp.updated_at > ?)
131136 OR
···511516func (s *GormStore) GetUncheckedDeadLinkURLs(ctx context.Context) ([]string, error) {
512517 var urls []string
513518 // Find URLs with cached errors in link_previews that have no row in archive_lookups
519519+ castType := "TEXT"
520520+ if s.db.Dialector.Name() == "mysql" {
521521+ castType = "CHAR"
522522+ }
514523 err := s.db.WithContext(ctx).Raw(`
515524 SELECT DISTINCT lp.url FROM link_previews lp
516516- WHERE CAST(lp.data AS TEXT) LIKE '%"error":%'
525525+ WHERE CAST(lp.data AS `+castType+`) LIKE '%"error":%'
517526 AND lp.url NOT IN (SELECT al.url FROM archive_lookups al)
518527 `).Scan(&urls).Error
519528 return urls, err