-
-
Notifications
You must be signed in to change notification settings - Fork 8
refactor: normalize schema and remove BCNF violations #689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e28897c
cd33d22
851096e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <<<<<<< SEARCH | ||
| const action = formData?.get('action') as string; | ||
| const drawnFeaturesString = formData?.get('drawnFeatures') as string; | ||
| let drawnFeatures: DrawnFeature[] = []; | ||
| try { | ||
| drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; | ||
| } catch (e) { | ||
| console.error('Failed to parse drawnFeatures:', e); | ||
| } | ||
| ======= | ||
| const action = formData?.get('action') as string; | ||
| const drawnFeaturesString = formData?.get('drawnFeatures') as string; | ||
| let drawnFeatures: DrawnFeature[] = []; | ||
| try { | ||
| drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; | ||
| } catch (e) { | ||
| console.error('Failed to parse drawnFeatures:', e); | ||
| } | ||
|
|
||
| const chatId = aiState.get().chatId; | ||
| if (chatId && drawnFeatures.length === 0) { | ||
| const drawingContext = await getDrawingContext(chatId); | ||
| if (drawingContext) { | ||
| drawnFeatures = (drawingContext as any).drawnFeatures || []; | ||
| } | ||
| } | ||
| >>>>>>> REPLACE |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| const action = formData?.get("action") as string; const drawnFeaturesString = formData?.get("drawnFeatures") as string; let drawnFeatures: DrawnFeature[] = []; try { drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; } catch (e) { console.error("Failed to parse drawnFeatures:", e); } const chatId = aiState.get().chatId; if (chatId && drawnFeatures.length === 0) { const drawingContext = await getDrawingContext(chatId); if (drawingContext) { drawnFeatures = (drawingContext as any).drawnFeatures || []; } } | ||
| console.error('Failed to parse drawnFeatures:', e); | ||
| } | ||
|
|
||
| if (action === 'generate_report_context') { | ||
| const messagesString = formData?.get('messages'); | ||
| if (typeof messagesString !== 'string') { | ||
| return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' }; | ||
| } | ||
| try { | ||
| const messages = JSON.parse(messagesString) as AIMessage[]; | ||
| return await generateReportContext(messages); | ||
| } catch (e) { | ||
| console.error('Failed to parse messages for report context:', e); | ||
| return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' }; | ||
| } | ||
| } | ||
|
|
||
| if (action === 'resolution_search') { | ||
| const file_mapbox = formData?.get('file_mapbox') as File; | ||
| const file_google = formData?.get('file_google') as File; | ||
| const file = (formData?.get('file') as File) || file_mapbox || file_google; | ||
| const timezone = (formData?.get('timezone') as string) || 'UTC'; | ||
| const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined; | ||
| const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined; | ||
| const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined; | ||
|
|
||
| if (!file) { | ||
| throw new Error('No file provided for resolution search.'); | ||
| } | ||
|
|
||
| const mapboxBuffer = file_mapbox ? await file_mapbox.arrayBuffer() : null; | ||
| const mapboxDataUrl = mapboxBuffer ? `data:${file_mapbox.type};base64,${Buffer.from(mapboxBuffer).toString('base64')}` : null; | ||
|
|
||
| const googleBuffer = file_google ? await file_google.arrayBuffer() : null; | ||
| const googleDataUrl = googleBuffer ? `data:${file_google.type};base64,${Buffer.from(googleBuffer).toString('base64')}` : null; | ||
|
|
||
| const buffer = await file.arrayBuffer(); | ||
| const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
|
|
||
| const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( | ||
| (message: any) => | ||
| message.role !== 'tool' && | ||
| message.type !== 'followup' && | ||
| message.type !== 'related' && | ||
| message.type !== 'end' && | ||
| message.type !== 'resolution_search_result' | ||
| ); | ||
|
|
||
| const userInput = 'Analyze this map view.'; | ||
| const content: CoreMessage['content'] = [ | ||
| { type: 'text', text: userInput }, | ||
| { type: 'image', image: dataUrl, mimeType: file.type } | ||
| ]; | ||
|
|
||
| aiState.update({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
| ...aiState.get().messages, | ||
| { id: nanoid(), role: 'user', content, type: 'input' } | ||
| ] | ||
| }); | ||
| messages.push({ role: 'user', content }); | ||
|
|
||
| const summaryStream = createStreamableValue<string>('Analyzing map view...'); | ||
| const groupeId = nanoid(); | ||
|
|
||
| async function processResolutionSearch() { | ||
| try { | ||
| const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); | ||
|
|
||
| let fullSummary = ''; | ||
| for await (const partialObject of streamResult.partialObjectStream) { | ||
| if (partialObject.summary) { | ||
| fullSummary = partialObject.summary; | ||
| summaryStream.update(fullSummary); | ||
| } | ||
| } | ||
|
|
||
| const analysisResult = await streamResult.object; | ||
| summaryStream.done(analysisResult.summary || 'Analysis complete.'); | ||
|
|
||
| // Reconstruct standard GeoJSON from flattened schema if present | ||
| let geoJson: FeatureCollection | null = null; | ||
| if (analysisResult.geoJson && analysisResult.geoJson.features) { | ||
| geoJson = { | ||
| type: 'FeatureCollection', | ||
| features: analysisResult.geoJson.features.map(f => ({ | ||
| type: 'Feature', | ||
| geometry: { | ||
| type: f.geometryType as any, | ||
| coordinates: f.coordinates as any | ||
| }, | ||
| properties: { | ||
| name: f.name, | ||
| description: f.description | ||
| } | ||
| })) | ||
| }; | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| -- Ensure system_prompts has a unique constraint on user_id | ||
| DO $$ | ||
| BEGIN | ||
| IF NOT EXISTS ( | ||
| SELECT 1 | ||
| FROM pg_constraint | ||
| WHERE conname = 'system_prompts_user_id_unique' | ||
| ) THEN | ||
| ALTER TABLE "system_prompts" ADD CONSTRAINT "system_prompts_user_id_unique" UNIQUE("user_id"); | ||
| END IF; | ||
|
Comment on lines
+4
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Deduplicate before adding the unique constraint. Line 9 can fail if the existing unconstrained Proposed migration hardening -- Ensure system_prompts has a unique constraint on user_id
+DELETE FROM "system_prompts" sp
+USING (
+ SELECT
+ "id",
+ ROW_NUMBER() OVER (
+ PARTITION BY "user_id"
+ ORDER BY "updated_at" DESC, "created_at" DESC, "id"
+ ) AS rn
+ FROM "system_prompts"
+) ranked
+WHERE sp."id" = ranked."id"
+ AND ranked.rn > 1;
+
DO $$
BEGIN🤖 Prompt for AI Agents |
||
| END $$; | ||
|
|
||
| -- Backfill existing users.system_prompt values into system_prompts | ||
| INSERT INTO "system_prompts" ("user_id", "prompt", "updated_at") | ||
| SELECT "id", "system_prompt", NOW() | ||
| FROM "users" | ||
| WHERE "system_prompt" IS NOT NULL | ||
| ON CONFLICT ("user_id") DO UPDATE | ||
| SET "prompt" = EXCLUDED."prompt", "updated_at" = EXCLUDED."updated_at"; | ||
|
|
||
| -- Drop the users.system_prompt column | ||
| ALTER TABLE "users" DROP COLUMN IF EXISTS "system_prompt"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| -- Convert existing geojson values into geometry for locations | ||
| UPDATE "locations" | ||
| SET "geometry" = ST_GeomFromGeoJSON("geojson"::text) | ||
| WHERE "geojson" IS NOT NULL AND "geometry" IS NULL; | ||
|
|
||
| -- Convert existing data values into geometry for visualizations (if they contain geometry) | ||
| UPDATE "visualizations" | ||
| SET "geometry" = ST_GeomFromGeoJSON("data"->>'geometry') | ||
| WHERE "data" IS NOT NULL AND "data"->>'geometry' IS NOT NULL AND "geometry" IS NULL; | ||
|
|
||
| -- Drop redundant column from locations | ||
| ALTER TABLE "locations" DROP COLUMN IF EXISTS "geojson"; | ||
|
|
||
| -- Clean up redundant geometry overlap in visualizations.data | ||
| -- We keep the data column but remove the geometry key from the jsonb object | ||
| UPDATE "visualizations" | ||
| SET "data" = "data" - 'geometry' | ||
| WHERE "data" IS NOT NULL AND "data" ? 'geometry'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| -- Drop derived path and share_path columns from chats | ||
| ALTER TABLE "chats" DROP COLUMN IF EXISTS "path"; | ||
| ALTER TABLE "chats" DROP COLUMN IF EXISTS "share_path"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| -- Delete existing synthetic calendar-note rows from messages | ||
| DELETE FROM "messages" | ||
| WHERE "role" = 'data' | ||
| AND "content"::jsonb->>'type' = 'calendar_note'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Avoid the empty-state flash on first open.
Line 26 now starts with
isLoading = false, so opening the sheet first rendersNo search historybeforefetchChats()switches to the spinner. That shows incorrect content on the initial open.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents