Complete database schema v1.0
- 26 tables for Goergens hierarchy + content + auth - Multi-level linking for images/documents/articles - Rights management (verified/uncertain/restricted) - Denormalized fields for performance - 3 materialized views for dashboard/search - Audit trail with history tables - Multilingual terminology system - PostgreSQL 16 features (JSONB, ranges, GIN/GIST indexes) Ready for Goergens data import (74,000+ cameras)
This commit is contained in:
parent
b52d3347e1
commit
10f7d7c8a4
|
|
@ -0,0 +1,218 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 1: Core Hierarchy
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- Enable required extensions
|
||||||
|
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- MANUFACTURERS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE manufacturers (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
goergens_code VARCHAR(4) UNIQUE NOT NULL,
|
||||||
|
name VARCHAR(200) NOT NULL,
|
||||||
|
country VARCHAR(3),
|
||||||
|
years_active INT4RANGE,
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by INT,
|
||||||
|
version INT DEFAULT 1,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT chk_mfg_code CHECK (goergens_code ~ '^[A-Z]{2,4}$')
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_mfg_name ON manufacturers(name);
|
||||||
|
CREATE INDEX idx_mfg_country ON manufacturers(country);
|
||||||
|
CREATE INDEX idx_mfg_active ON manufacturers(id) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE manufacturers IS 'Camera manufacturers - top level of Goergens hierarchy';
|
||||||
|
COMMENT ON COLUMN manufacturers.goergens_code IS 'Goergens manufacturer code (e.g., ERNM, ZEII)';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CAMERA MODELS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE camera_models (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
manufacturer_id INT NOT NULL REFERENCES manufacturers(id),
|
||||||
|
goergens_model_number VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
-- Goergens classification codes
|
||||||
|
body_type VARCHAR(2) NOT NULL,
|
||||||
|
viewfinder_type VARCHAR(4),
|
||||||
|
format_code VARCHAR(20) NOT NULL,
|
||||||
|
|
||||||
|
-- Production years
|
||||||
|
year_first INT,
|
||||||
|
year_last INT,
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by INT,
|
||||||
|
version INT DEFAULT 1,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE(manufacturer_id, goergens_model_number),
|
||||||
|
CONSTRAINT chk_years CHECK (year_first IS NULL OR year_last IS NULL OR year_first <= year_last),
|
||||||
|
CONSTRAINT chk_body_type CHECK (body_type ~ '^[A-Z][a-z]?$')
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_models_manufacturer ON camera_models(manufacturer_id);
|
||||||
|
CREATE INDEX idx_models_type ON camera_models(body_type, viewfinder_type);
|
||||||
|
CREATE INDEX idx_models_format ON camera_models(format_code);
|
||||||
|
CREATE INDEX idx_models_years ON camera_models(year_first, year_last);
|
||||||
|
CREATE INDEX idx_models_active ON camera_models(id) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE camera_models IS 'Camera models - grouping level in Goergens system';
|
||||||
|
COMMENT ON COLUMN camera_models.goergens_model_number IS 'Model number from Goergens ID (e.g., 1910 for Bob IV)';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CAMERA MODEL I18N
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE camera_model_i18n (
|
||||||
|
camera_model_id INT PRIMARY KEY REFERENCES camera_models(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
model_name VARCHAR(200),
|
||||||
|
description_de TEXT,
|
||||||
|
description_en TEXT,
|
||||||
|
marketing_name_de VARCHAR(200),
|
||||||
|
marketing_name_en VARCHAR(200),
|
||||||
|
|
||||||
|
-- Full-text search vectors
|
||||||
|
search_vector_de TSVECTOR GENERATED ALWAYS AS (
|
||||||
|
to_tsvector('german', COALESCE(model_name, '') || ' ' || COALESCE(description_de, ''))
|
||||||
|
) STORED,
|
||||||
|
search_vector_en TSVECTOR GENERATED ALWAYS AS (
|
||||||
|
to_tsvector('english', COALESCE(model_name, '') || ' ' || COALESCE(description_en, ''))
|
||||||
|
) STORED
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_i18n_fts_de ON camera_model_i18n USING GIN(search_vector_de);
|
||||||
|
CREATE INDEX idx_i18n_fts_en ON camera_model_i18n USING GIN(search_vector_en);
|
||||||
|
|
||||||
|
COMMENT ON TABLE camera_model_i18n IS 'Multilingual content for camera models';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- HOUSING VARIANTS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE housing_variants (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
camera_model_id INT NOT NULL REFERENCES camera_models(id) ON DELETE CASCADE,
|
||||||
|
goergens_variant_letter VARCHAR(2) NOT NULL,
|
||||||
|
|
||||||
|
color VARCHAR(50),
|
||||||
|
body_material VARCHAR(50),
|
||||||
|
distinguishing_features TEXT,
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by INT,
|
||||||
|
version INT DEFAULT 1,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE(camera_model_id, goergens_variant_letter),
|
||||||
|
CONSTRAINT chk_variant_letter CHECK (goergens_variant_letter ~ '^[a-z]{1,2}$')
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_variants_model ON housing_variants(camera_model_id);
|
||||||
|
CREATE INDEX idx_variants_active ON housing_variants(id) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE housing_variants IS 'Housing variants - color, material variations';
|
||||||
|
COMMENT ON COLUMN housing_variants.goergens_variant_letter IS 'Variant letter from Goergens ID (a, b, c, etc.)';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- LENS/SHUTTER COMBINATIONS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE lens_shutter_combos (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
housing_variant_id INT NOT NULL REFERENCES housing_variants(id) ON DELETE CASCADE,
|
||||||
|
goergens_combo_number VARCHAR(3) NOT NULL,
|
||||||
|
|
||||||
|
lens_id INT,
|
||||||
|
shutter_id INT,
|
||||||
|
|
||||||
|
-- DENORMALIZED FIELDS FOR PERFORMANCE
|
||||||
|
manufacturer_id INT,
|
||||||
|
camera_model_id INT,
|
||||||
|
goergens_full_id VARCHAR(50),
|
||||||
|
display_name TEXT,
|
||||||
|
|
||||||
|
-- STRUCTURED TECHNICAL SPECS
|
||||||
|
weight_g INT,
|
||||||
|
width_mm DECIMAL(6,2),
|
||||||
|
height_mm DECIMAL(6,2),
|
||||||
|
depth_mm DECIMAL(6,2),
|
||||||
|
|
||||||
|
-- FLEXIBLE SPECS (JSONB)
|
||||||
|
specs JSONB,
|
||||||
|
|
||||||
|
-- Full-text search
|
||||||
|
search_text TEXT,
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by INT,
|
||||||
|
version INT DEFAULT 1,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE(housing_variant_id, goergens_combo_number),
|
||||||
|
CONSTRAINT chk_weight CHECK (weight_g IS NULL OR (weight_g > 0 AND weight_g < 50000)),
|
||||||
|
CONSTRAINT chk_dimensions CHECK (
|
||||||
|
width_mm IS NULL OR height_mm IS NULL OR depth_mm IS NULL OR
|
||||||
|
(width_mm > 0 AND height_mm > 0 AND depth_mm > 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Critical indexes
|
||||||
|
CREATE INDEX idx_combos_variant ON lens_shutter_combos(housing_variant_id);
|
||||||
|
CREATE INDEX idx_combos_lens ON lens_shutter_combos(lens_id);
|
||||||
|
CREATE INDEX idx_combos_shutter ON lens_shutter_combos(shutter_id);
|
||||||
|
|
||||||
|
-- Denormalized indexes (performance boost)
|
||||||
|
CREATE INDEX idx_combos_manufacturer ON lens_shutter_combos(manufacturer_id);
|
||||||
|
CREATE INDEX idx_combos_model ON lens_shutter_combos(camera_model_id);
|
||||||
|
CREATE INDEX idx_combos_goergens_id ON lens_shutter_combos(goergens_full_id);
|
||||||
|
|
||||||
|
-- JSONB and full-text search
|
||||||
|
CREATE INDEX idx_combos_specs ON lens_shutter_combos USING GIN(specs);
|
||||||
|
CREATE INDEX idx_combos_fts ON lens_shutter_combos USING GIN(to_tsvector('german', COALESCE(search_text, '')));
|
||||||
|
CREATE INDEX idx_combos_active ON lens_shutter_combos(id) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE lens_shutter_combos IS 'Lens/shutter combinations - lowest level of hierarchy';
|
||||||
|
COMMENT ON COLUMN lens_shutter_combos.goergens_full_id IS 'Complete Goergens ID (e.g., ERNM K 1910 a01)';
|
||||||
|
COMMENT ON COLUMN lens_shutter_combos.specs IS 'Flexible technical specs as JSON (body_material, bellows_color, etc.)';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CAMERA RELATIONSHIPS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE camera_relations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
source_combo_id INT NOT NULL REFERENCES lens_shutter_combos(id) ON DELETE CASCADE,
|
||||||
|
related_combo_id INT NOT NULL REFERENCES lens_shutter_combos(id) ON DELETE CASCADE,
|
||||||
|
relationship_type VARCHAR(50) NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
|
||||||
|
UNIQUE(source_combo_id, related_combo_id, relationship_type),
|
||||||
|
CONSTRAINT chk_not_self CHECK (source_combo_id != related_combo_id),
|
||||||
|
CONSTRAINT chk_relationship_type CHECK (relationship_type IN ('successor', 'predecessor', 'similar', 'variant_of', 'replaced_by'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_relations_source ON camera_relations(source_combo_id);
|
||||||
|
CREATE INDEX idx_relations_target ON camera_relations(related_combo_id);
|
||||||
|
CREATE INDEX idx_relations_type ON camera_relations(relationship_type);
|
||||||
|
|
||||||
|
COMMENT ON TABLE camera_relations IS 'Relationships between cameras (evolution, variants, etc.)';
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 2: Supporting Data
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- LENSES
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE lenses (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(200) NOT NULL,
|
||||||
|
manufacturer VARCHAR(100),
|
||||||
|
focal_length_mm INT,
|
||||||
|
max_aperture DECIMAL(3,1),
|
||||||
|
lens_type VARCHAR(50),
|
||||||
|
mount_type VARCHAR(50),
|
||||||
|
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by INT,
|
||||||
|
|
||||||
|
CONSTRAINT chk_focal_length CHECK (focal_length_mm IS NULL OR focal_length_mm > 0),
|
||||||
|
CONSTRAINT chk_aperture CHECK (max_aperture IS NULL OR (max_aperture > 0 AND max_aperture < 32))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lenses_name ON lenses(name);
|
||||||
|
CREATE INDEX idx_lenses_mfg ON lenses(manufacturer);
|
||||||
|
CREATE INDEX idx_lenses_focal ON lenses(focal_length_mm);
|
||||||
|
CREATE INDEX idx_lenses_aperture ON lenses(max_aperture);
|
||||||
|
|
||||||
|
COMMENT ON TABLE lenses IS 'Lens catalog - reusable across cameras';
|
||||||
|
|
||||||
|
-- Add foreign key to lens_shutter_combos
|
||||||
|
ALTER TABLE lens_shutter_combos
|
||||||
|
ADD CONSTRAINT fk_lens FOREIGN KEY (lens_id) REFERENCES lenses(id);
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- SHUTTERS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE shutters (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
manufacturer VARCHAR(100),
|
||||||
|
shutter_type VARCHAR(50),
|
||||||
|
speed_range VARCHAR(100),
|
||||||
|
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
updated_by INT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shutters_name ON shutters(name);
|
||||||
|
CREATE INDEX idx_shutters_type ON shutters(shutter_type);
|
||||||
|
|
||||||
|
COMMENT ON TABLE shutters IS 'Shutter catalog - reusable across cameras';
|
||||||
|
|
||||||
|
-- Add foreign key to lens_shutter_combos
|
||||||
|
ALTER TABLE lens_shutter_combos
|
||||||
|
ADD CONSTRAINT fk_shutter FOREIGN KEY (shutter_id) REFERENCES shutters(id);
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- FILM FORMATS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE formats (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
format_code VARCHAR(20) UNIQUE NOT NULL,
|
||||||
|
width_mm DECIMAL(6,2),
|
||||||
|
height_mm DECIMAL(6,2),
|
||||||
|
format_type VARCHAR(20),
|
||||||
|
|
||||||
|
description_de TEXT,
|
||||||
|
description_en TEXT,
|
||||||
|
|
||||||
|
sort_order INT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_formats_code ON formats(format_code);
|
||||||
|
CREATE INDEX idx_formats_type ON formats(format_type);
|
||||||
|
|
||||||
|
COMMENT ON TABLE formats IS 'Film format definitions';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CONCEPTS (Multilingual Terminology)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE concepts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
concept_code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
category VARCHAR(50) NOT NULL,
|
||||||
|
sort_order INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE concepts IS 'Terminology concepts - language-agnostic';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CONCEPT TERMS (Translations)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE concept_terms (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
concept_id INT NOT NULL REFERENCES concepts(id) ON DELETE CASCADE,
|
||||||
|
language_code VARCHAR(5) NOT NULL,
|
||||||
|
term VARCHAR(200) NOT NULL,
|
||||||
|
term_type VARCHAR(20) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(concept_id, language_code, term_type),
|
||||||
|
CONSTRAINT chk_term_type CHECK (term_type IN ('primary', 'abbreviation', 'synonym'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_terms_concept ON concept_terms(concept_id);
|
||||||
|
CREATE INDEX idx_terms_language ON concept_terms(language_code);
|
||||||
|
CREATE INDEX idx_terms_term ON concept_terms(term);
|
||||||
|
|
||||||
|
COMMENT ON TABLE concept_terms IS 'Multilingual terms for concepts';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- TERMINOLOGY VIEW (Backwards Compatibility)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE VIEW terminology AS
|
||||||
|
SELECT
|
||||||
|
c.concept_code,
|
||||||
|
c.category,
|
||||||
|
MAX(CASE WHEN ct.language_code = 'de' AND ct.term_type = 'primary' THEN ct.term END) AS term_de,
|
||||||
|
MAX(CASE WHEN ct.language_code = 'en' AND ct.term_type = 'primary' THEN ct.term END) AS term_en,
|
||||||
|
MAX(CASE WHEN ct.language_code = 'de' AND ct.term_type = 'abbreviation' THEN ct.term END) AS abbreviation
|
||||||
|
FROM concepts c
|
||||||
|
LEFT JOIN concept_terms ct ON c.id = ct.concept_id
|
||||||
|
GROUP BY c.concept_code, c.category;
|
||||||
|
|
||||||
|
COMMENT ON VIEW terminology IS 'Simple view of German/English terms for quick lookups';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- SERIAL NUMBERS (Authentication Database)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE serial_numbers (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
camera_model_id INT NOT NULL REFERENCES camera_models(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- PostgreSQL range types for proper validation
|
||||||
|
serial_range INT4RANGE NOT NULL,
|
||||||
|
year_range INT4RANGE NOT NULL,
|
||||||
|
|
||||||
|
variant_notes VARCHAR(200),
|
||||||
|
production_notes TEXT,
|
||||||
|
source VARCHAR(200),
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
|
||||||
|
-- Prevent overlapping serial ranges
|
||||||
|
CONSTRAINT no_serial_overlap EXCLUDE USING GIST (
|
||||||
|
camera_model_id WITH =,
|
||||||
|
serial_range WITH &&
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_serials_model ON serial_numbers(camera_model_id);
|
||||||
|
CREATE INDEX idx_serials_range ON serial_numbers USING GIST(serial_range);
|
||||||
|
CREATE INDEX idx_serials_year ON serial_numbers USING GIST(year_range);
|
||||||
|
|
||||||
|
COMMENT ON TABLE serial_numbers IS 'Serial number ranges for camera authentication';
|
||||||
|
COMMENT ON COLUMN serial_numbers.serial_range IS 'Query: WHERE serial_range @> 52103 to check if serial is genuine';
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 3: Content & Media
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CONTENT SOURCES (Provenance Tracking)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE content_sources (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
source_name VARCHAR(100) NOT NULL,
|
||||||
|
source_type VARCHAR(50) NOT NULL,
|
||||||
|
rights_status VARCHAR(50) NOT NULL,
|
||||||
|
legal_notes TEXT,
|
||||||
|
contact_info TEXT,
|
||||||
|
verified_by VARCHAR(100),
|
||||||
|
verified_date DATE,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT chk_source_type CHECK (source_type IN ('internal', 'licensed', 'uncertain', 'public_domain')),
|
||||||
|
CONSTRAINT chk_rights_status CHECK (rights_status IN ('owned', 'licensed', 'questionable', 'public_domain'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE content_sources IS 'Provenance tracking for all content';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- IMAGES (Unified with Rights Status)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE images (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
filename VARCHAR(255) NOT NULL,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
caption TEXT,
|
||||||
|
|
||||||
|
-- FLEXIBLE MULTI-LEVEL LINKING (only ONE should be set)
|
||||||
|
manufacturer_id INT REFERENCES manufacturers(id),
|
||||||
|
camera_model_id INT REFERENCES camera_models(id),
|
||||||
|
housing_variant_id INT REFERENCES housing_variants(id),
|
||||||
|
combo_id INT REFERENCES lens_shutter_combos(id),
|
||||||
|
|
||||||
|
-- RIGHTS MANAGEMENT
|
||||||
|
rights_status VARCHAR(20) NOT NULL,
|
||||||
|
source_id INT NOT NULL REFERENCES content_sources(id),
|
||||||
|
|
||||||
|
-- Verified images
|
||||||
|
photographer VARCHAR(100),
|
||||||
|
photo_date DATE,
|
||||||
|
rights_holder VARCHAR(200),
|
||||||
|
license_type VARCHAR(50),
|
||||||
|
|
||||||
|
-- Uncertain images
|
||||||
|
why_uncertain TEXT,
|
||||||
|
usage_restriction VARCHAR(50),
|
||||||
|
clearance_status VARCHAR(50),
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
file_size_kb INT,
|
||||||
|
width_px INT,
|
||||||
|
height_px INT,
|
||||||
|
mime_type VARCHAR(50),
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
uploaded_by INT,
|
||||||
|
upload_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
verified_clean BOOLEAN DEFAULT FALSE,
|
||||||
|
verified_by VARCHAR(100),
|
||||||
|
verified_date DATE,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
-- Exactly ONE link must be set
|
||||||
|
CONSTRAINT chk_image_link CHECK (
|
||||||
|
(manufacturer_id IS NOT NULL AND camera_model_id IS NULL AND housing_variant_id IS NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NOT NULL AND housing_variant_id IS NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NULL AND housing_variant_id IS NOT NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NULL AND housing_variant_id IS NULL AND combo_id IS NOT NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_rights_status CHECK (rights_status IN ('verified', 'uncertain', 'restricted')),
|
||||||
|
CONSTRAINT chk_uncertain_fields CHECK (
|
||||||
|
(rights_status = 'uncertain' AND why_uncertain IS NOT NULL) OR
|
||||||
|
(rights_status = 'verified' AND why_uncertain IS NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_sort_order CHECK (sort_order >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for each link type
|
||||||
|
CREATE INDEX idx_images_manufacturer ON images(manufacturer_id) WHERE manufacturer_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_images_model ON images(camera_model_id) WHERE camera_model_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_images_variant ON images(housing_variant_id) WHERE housing_variant_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_images_combo ON images(combo_id) WHERE combo_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_images_source ON images(source_id);
|
||||||
|
CREATE INDEX idx_images_status ON images(rights_status);
|
||||||
|
|
||||||
|
-- Partial indexes for performance
|
||||||
|
CREATE INDEX idx_images_verified_model ON images(camera_model_id, sort_order)
|
||||||
|
WHERE rights_status = 'verified' AND camera_model_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_images_uncertain ON images(camera_model_id)
|
||||||
|
WHERE rights_status = 'uncertain' AND camera_model_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_images_primary ON images(camera_model_id)
|
||||||
|
WHERE sort_order = 0 AND rights_status = 'verified' AND camera_model_id IS NOT NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE images IS 'Images with multi-level linking and rights management';
|
||||||
|
COMMENT ON COLUMN images.rights_status IS 'verified=publishable, uncertain=research only, restricted=internal only';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- DOCUMENTS (Manuals, Brochures, etc.)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE documents (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- FLEXIBLE MULTI-LEVEL LINKING (only ONE should be set)
|
||||||
|
manufacturer_id INT REFERENCES manufacturers(id),
|
||||||
|
camera_model_id INT REFERENCES camera_models(id),
|
||||||
|
housing_variant_id INT REFERENCES housing_variants(id),
|
||||||
|
combo_id INT REFERENCES lens_shutter_combos(id),
|
||||||
|
|
||||||
|
-- Document classification
|
||||||
|
document_type VARCHAR(50) NOT NULL,
|
||||||
|
|
||||||
|
file_path VARCHAR(500),
|
||||||
|
title TEXT,
|
||||||
|
language VARCHAR(5),
|
||||||
|
page_count INT,
|
||||||
|
publication_date DATE,
|
||||||
|
|
||||||
|
-- Rights
|
||||||
|
rights_status VARCHAR(20) DEFAULT 'safe',
|
||||||
|
source_id INT REFERENCES content_sources(id),
|
||||||
|
why_safe TEXT,
|
||||||
|
|
||||||
|
-- Metadata
|
||||||
|
file_size_kb INT,
|
||||||
|
mime_type VARCHAR(50),
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
uploaded_by INT,
|
||||||
|
upload_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
-- Exactly ONE link must be set
|
||||||
|
CONSTRAINT chk_doc_link CHECK (
|
||||||
|
(manufacturer_id IS NOT NULL AND camera_model_id IS NULL AND housing_variant_id IS NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NOT NULL AND housing_variant_id IS NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NULL AND housing_variant_id IS NOT NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NULL AND housing_variant_id IS NULL AND combo_id IS NOT NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_doc_type CHECK (document_type IN (
|
||||||
|
'user_manual', 'repair_manual', 'brochure', 'advertisement',
|
||||||
|
'price_list', 'catalog', 'technical_spec', 'patent', 'parts_list'
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for each link type
|
||||||
|
CREATE INDEX idx_docs_manufacturer ON documents(manufacturer_id) WHERE manufacturer_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_docs_model ON documents(camera_model_id) WHERE camera_model_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_docs_variant ON documents(housing_variant_id) WHERE housing_variant_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_docs_combo ON documents(combo_id) WHERE combo_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_docs_type ON documents(document_type);
|
||||||
|
CREATE INDEX idx_docs_language ON documents(language);
|
||||||
|
|
||||||
|
COMMENT ON TABLE documents IS 'Documents with multi-level linking - manuals, brochures, repair manuals, etc.';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- ARTICLES
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE articles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Publication info
|
||||||
|
publication_code VARCHAR(10),
|
||||||
|
issue_number INT,
|
||||||
|
year INT,
|
||||||
|
page INT,
|
||||||
|
|
||||||
|
-- Content
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
author VARCHAR(200),
|
||||||
|
language VARCHAR(5) DEFAULT 'de',
|
||||||
|
|
||||||
|
content TEXT,
|
||||||
|
abstract TEXT,
|
||||||
|
|
||||||
|
-- Rights management
|
||||||
|
rights_status VARCHAR(20) NOT NULL DEFAULT 'verified',
|
||||||
|
source_id INT REFERENCES content_sources(id),
|
||||||
|
publication_owns_rights BOOLEAN DEFAULT FALSE,
|
||||||
|
digitization_rights_cleared BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
why_uncertain TEXT,
|
||||||
|
usage_restriction VARCHAR(50),
|
||||||
|
|
||||||
|
-- Full-text search
|
||||||
|
search_vector TSVECTOR GENERATED ALWAYS AS (
|
||||||
|
to_tsvector('german', title || ' ' || COALESCE(author, '') || ' ' || COALESCE(abstract, ''))
|
||||||
|
) STORED,
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
digitized_by VARCHAR(100),
|
||||||
|
digitized_date DATE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT chk_article_rights CHECK (
|
||||||
|
(rights_status = 'uncertain' AND content IS NULL) OR
|
||||||
|
(rights_status = 'verified')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_articles_pub ON articles(publication_code, issue_number);
|
||||||
|
CREATE INDEX idx_articles_year ON articles(year);
|
||||||
|
CREATE INDEX idx_articles_author ON articles(author);
|
||||||
|
CREATE INDEX idx_articles_status ON articles(rights_status);
|
||||||
|
CREATE INDEX idx_articles_fts ON articles USING GIN(search_vector);
|
||||||
|
|
||||||
|
COMMENT ON TABLE articles IS 'Magazine articles - Photo Antiquaria, Club Daguerre aktuell, etc.';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CAMERA-ARTICLE LINKS (Many-to-Many with Multi-Level)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE camera_articles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
article_id INT NOT NULL REFERENCES articles(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- FLEXIBLE MULTI-LEVEL LINKING (only ONE should be set)
|
||||||
|
manufacturer_id INT REFERENCES manufacturers(id),
|
||||||
|
camera_model_id INT REFERENCES camera_models(id),
|
||||||
|
housing_variant_id INT REFERENCES housing_variants(id),
|
||||||
|
combo_id INT REFERENCES lens_shutter_combos(id),
|
||||||
|
|
||||||
|
relevance VARCHAR(20),
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by INT,
|
||||||
|
|
||||||
|
-- Exactly ONE link must be set
|
||||||
|
CONSTRAINT chk_article_link CHECK (
|
||||||
|
(manufacturer_id IS NOT NULL AND camera_model_id IS NULL AND housing_variant_id IS NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NOT NULL AND housing_variant_id IS NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NULL AND housing_variant_id IS NOT NULL AND combo_id IS NULL) OR
|
||||||
|
(manufacturer_id IS NULL AND camera_model_id IS NULL AND housing_variant_id IS NULL AND combo_id IS NOT NULL)
|
||||||
|
),
|
||||||
|
CONSTRAINT chk_relevance CHECK (relevance IN ('primary', 'mentioned', 'comparison')),
|
||||||
|
UNIQUE(article_id, manufacturer_id, camera_model_id, housing_variant_id, combo_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ca_article ON camera_articles(article_id);
|
||||||
|
CREATE INDEX idx_ca_manufacturer ON camera_articles(manufacturer_id) WHERE manufacturer_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_ca_model ON camera_articles(camera_model_id) WHERE camera_model_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_ca_variant ON camera_articles(housing_variant_id) WHERE housing_variant_id IS NOT NULL;
|
||||||
|
CREATE INDEX idx_ca_combo ON camera_articles(combo_id) WHERE combo_id IS NOT NULL;
|
||||||
|
|
||||||
|
COMMENT ON TABLE camera_articles IS 'Links articles to cameras at appropriate hierarchy level';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- ANNOTATIONS (User Contributions)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE annotations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
combo_id INT NOT NULL REFERENCES lens_shutter_combos(id) ON DELETE CASCADE,
|
||||||
|
annotation_type VARCHAR(50) NOT NULL,
|
||||||
|
|
||||||
|
-- Bilingual content
|
||||||
|
content_de TEXT,
|
||||||
|
content_en TEXT,
|
||||||
|
source_language VARCHAR(5),
|
||||||
|
|
||||||
|
-- Attribution
|
||||||
|
contributed_by INT,
|
||||||
|
contribution_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Moderation
|
||||||
|
approved BOOLEAN DEFAULT FALSE,
|
||||||
|
approved_by INT,
|
||||||
|
approved_date TIMESTAMP,
|
||||||
|
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT chk_annotation_type CHECK (annotation_type IN ('historical_note', 'anecdote', 'provenance', 'usage_note', 'technical_note'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_annotations_combo ON annotations(combo_id);
|
||||||
|
CREATE INDEX idx_annotations_type ON annotations(annotation_type);
|
||||||
|
CREATE INDEX idx_annotations_approved ON annotations(approved);
|
||||||
|
|
||||||
|
COMMENT ON TABLE annotations IS 'User-contributed notes and information';
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 4: Authentication & Members
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- MEMBERS (Tier 1 Authentication)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE members (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
member_number VARCHAR(20) UNIQUE,
|
||||||
|
email VARCHAR(200) UNIQUE NOT NULL,
|
||||||
|
name VARCHAR(200) NOT NULL,
|
||||||
|
password_hash VARCHAR(255),
|
||||||
|
|
||||||
|
-- Account status
|
||||||
|
account_active BOOLEAN DEFAULT TRUE,
|
||||||
|
member_since DATE,
|
||||||
|
membership_expires DATE,
|
||||||
|
|
||||||
|
-- Permissions
|
||||||
|
can_view_public BOOLEAN DEFAULT TRUE,
|
||||||
|
can_view_member_content BOOLEAN DEFAULT TRUE,
|
||||||
|
can_contribute BOOLEAN DEFAULT FALSE,
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
last_login TIMESTAMP,
|
||||||
|
login_count INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_members_email ON members(email);
|
||||||
|
CREATE INDEX idx_members_number ON members(member_number);
|
||||||
|
CREATE INDEX idx_members_active ON members(id) WHERE account_active = TRUE;
|
||||||
|
|
||||||
|
COMMENT ON TABLE members IS 'Club members - basic authentication and permissions';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- RESEARCH ACCESS (Tier 2 Authentication)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE research_access (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
member_id INT NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
has_research_access BOOLEAN DEFAULT FALSE,
|
||||||
|
access_granted_by VARCHAR(100),
|
||||||
|
access_granted_date DATE,
|
||||||
|
access_expires DATE,
|
||||||
|
|
||||||
|
research_purpose TEXT,
|
||||||
|
institution VARCHAR(200),
|
||||||
|
|
||||||
|
-- Usage tracking
|
||||||
|
access_count INT DEFAULT 0,
|
||||||
|
last_research_access TIMESTAMP,
|
||||||
|
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_research_member ON research_access(member_id);
|
||||||
|
CREATE INDEX idx_research_active ON research_access(member_id)
|
||||||
|
WHERE has_research_access = TRUE AND (access_expires IS NULL OR access_expires > CURRENT_DATE);
|
||||||
|
|
||||||
|
COMMENT ON TABLE research_access IS 'Research access grants for viewing uncertain content';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- RESEARCH SESSIONS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE research_sessions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
member_id INT NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
||||||
|
session_token VARCHAR(64) UNIQUE NOT NULL,
|
||||||
|
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
last_activity TIMESTAMP,
|
||||||
|
|
||||||
|
resources_accessed TEXT[]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_sessions_member ON research_sessions(member_id);
|
||||||
|
CREATE INDEX idx_sessions_token ON research_sessions(session_token);
|
||||||
|
CREATE INDEX idx_sessions_active ON research_sessions(expires_at)
|
||||||
|
WHERE expires_at > NOW();
|
||||||
|
|
||||||
|
COMMENT ON TABLE research_sessions IS 'Active research sessions for audit trail';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- SYSTEM CONFIG (Shared Settings)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE system_config (
|
||||||
|
key VARCHAR(100) PRIMARY KEY,
|
||||||
|
value TEXT,
|
||||||
|
description TEXT,
|
||||||
|
updated_by VARCHAR(100),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE system_config IS 'System-wide configuration (including research password hash)';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- AUDIT LOG
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE audit_log (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
member_id INT REFERENCES members(id),
|
||||||
|
|
||||||
|
action VARCHAR(100) NOT NULL,
|
||||||
|
resource_type VARCHAR(50),
|
||||||
|
resource_id INT,
|
||||||
|
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
|
||||||
|
details JSONB,
|
||||||
|
|
||||||
|
timestamp TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_audit_member ON audit_log(member_id);
|
||||||
|
CREATE INDEX idx_audit_action ON audit_log(action);
|
||||||
|
CREATE INDEX idx_audit_timestamp ON audit_log(timestamp DESC);
|
||||||
|
CREATE INDEX idx_audit_resource ON audit_log(resource_type, resource_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit_log IS 'Comprehensive audit log for all user actions';
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 5: History Tables (Audit Trail)
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CAMERA MODEL HISTORY
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE camera_models_history (
|
||||||
|
history_id BIGSERIAL PRIMARY KEY,
|
||||||
|
id INT NOT NULL,
|
||||||
|
manufacturer_id INT,
|
||||||
|
goergens_model_number VARCHAR(10),
|
||||||
|
body_type VARCHAR(2),
|
||||||
|
viewfinder_type VARCHAR(4),
|
||||||
|
format_code VARCHAR(20),
|
||||||
|
year_first INT,
|
||||||
|
year_last INT,
|
||||||
|
|
||||||
|
-- History metadata
|
||||||
|
version INT NOT NULL,
|
||||||
|
changed_at TIMESTAMP NOT NULL,
|
||||||
|
changed_by INT,
|
||||||
|
change_type VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(id, version),
|
||||||
|
CONSTRAINT chk_change_type CHECK (change_type IN ('INSERT', 'UPDATE', 'DELETE'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_models_hist_id ON camera_models_history(id);
|
||||||
|
CREATE INDEX idx_models_hist_time ON camera_models_history(changed_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON TABLE camera_models_history IS 'Historical versions of camera models';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- LENS/SHUTTER COMBO HISTORY
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE lens_shutter_combos_history (
|
||||||
|
history_id BIGSERIAL PRIMARY KEY,
|
||||||
|
id INT NOT NULL,
|
||||||
|
housing_variant_id INT,
|
||||||
|
goergens_combo_number VARCHAR(3),
|
||||||
|
lens_id INT,
|
||||||
|
shutter_id INT,
|
||||||
|
weight_g INT,
|
||||||
|
width_mm DECIMAL(6,2),
|
||||||
|
height_mm DECIMAL(6,2),
|
||||||
|
depth_mm DECIMAL(6,2),
|
||||||
|
specs JSONB,
|
||||||
|
|
||||||
|
version INT NOT NULL,
|
||||||
|
changed_at TIMESTAMP NOT NULL,
|
||||||
|
changed_by INT,
|
||||||
|
change_type VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(id, version),
|
||||||
|
CONSTRAINT chk_change_type CHECK (change_type IN ('INSERT', 'UPDATE', 'DELETE'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_combos_hist_id ON lens_shutter_combos_history(id);
|
||||||
|
CREATE INDEX idx_combos_hist_time ON lens_shutter_combos_history(changed_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON TABLE lens_shutter_combos_history IS 'Historical versions of camera combinations';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- ANNOTATION HISTORY
|
||||||
|
-- ==========================================
|
||||||
|
CREATE TABLE annotations_history (
|
||||||
|
history_id BIGSERIAL PRIMARY KEY,
|
||||||
|
id INT NOT NULL,
|
||||||
|
combo_id INT,
|
||||||
|
annotation_type VARCHAR(50),
|
||||||
|
content_de TEXT,
|
||||||
|
content_en TEXT,
|
||||||
|
contributed_by INT,
|
||||||
|
|
||||||
|
version INT NOT NULL,
|
||||||
|
changed_at TIMESTAMP NOT NULL,
|
||||||
|
changed_by INT,
|
||||||
|
change_type VARCHAR(10) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE(id, version),
|
||||||
|
CONSTRAINT chk_change_type CHECK (change_type IN ('INSERT', 'UPDATE', 'DELETE'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_annotations_hist_id ON annotations_history(id);
|
||||||
|
CREATE INDEX idx_annotations_hist_time ON annotations_history(changed_at DESC);
|
||||||
|
|
||||||
|
COMMENT ON TABLE annotations_history IS 'Historical versions of user annotations';
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 6: Triggers & Functions
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- FUNCTION: Update Denormalized Fields
|
||||||
|
-- ==========================================
|
||||||
|
CREATE OR REPLACE FUNCTION update_combo_denormalized()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Get hierarchy IDs
|
||||||
|
SELECT
|
||||||
|
hv.camera_model_id,
|
||||||
|
cm.manufacturer_id
|
||||||
|
INTO
|
||||||
|
NEW.camera_model_id,
|
||||||
|
NEW.manufacturer_id
|
||||||
|
FROM housing_variants hv
|
||||||
|
JOIN camera_models cm ON hv.camera_model_id = cm.id
|
||||||
|
WHERE hv.id = NEW.housing_variant_id;
|
||||||
|
|
||||||
|
-- Generate Goergens ID
|
||||||
|
SELECT
|
||||||
|
m.goergens_code || ' K ' || cm.goergens_model_number || ' ' ||
|
||||||
|
hv.goergens_variant_letter || NEW.goergens_combo_number
|
||||||
|
INTO NEW.goergens_full_id
|
||||||
|
FROM housing_variants hv
|
||||||
|
JOIN camera_models cm ON hv.camera_model_id = cm.id
|
||||||
|
JOIN manufacturers m ON cm.manufacturer_id = m.id
|
||||||
|
WHERE hv.id = NEW.housing_variant_id;
|
||||||
|
|
||||||
|
-- Generate display name
|
||||||
|
SELECT
|
||||||
|
m.name || ' ' ||
|
||||||
|
COALESCE(i18n.model_name, '') ||
|
||||||
|
CASE WHEN hv.color IS NOT NULL THEN ' (' || hv.color || ')' ELSE '' END ||
|
||||||
|
CASE WHEN l.name IS NOT NULL THEN ' ' || l.name ELSE '' END ||
|
||||||
|
CASE WHEN l.focal_length_mm IS NOT NULL THEN ' ' || l.focal_length_mm || 'mm' ELSE '' END ||
|
||||||
|
CASE WHEN s.name IS NOT NULL THEN ' ' || s.name ELSE '' END
|
||||||
|
INTO NEW.display_name
|
||||||
|
FROM housing_variants hv
|
||||||
|
JOIN camera_models cm ON hv.camera_model_id = cm.id
|
||||||
|
JOIN manufacturers m ON cm.manufacturer_id = m.id
|
||||||
|
LEFT JOIN camera_model_i18n i18n ON cm.id = i18n.camera_model_id
|
||||||
|
LEFT JOIN lenses l ON NEW.lens_id = l.id
|
||||||
|
LEFT JOIN shutters s ON NEW.shutter_id = s.id
|
||||||
|
WHERE hv.id = NEW.housing_variant_id;
|
||||||
|
|
||||||
|
-- Generate search text
|
||||||
|
NEW.search_text := NEW.display_name || ' ' || COALESCE(NEW.goergens_full_id, '');
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Trigger on INSERT
|
||||||
|
CREATE TRIGGER trg_combo_denorm_insert
|
||||||
|
BEFORE INSERT ON lens_shutter_combos
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_combo_denormalized();
|
||||||
|
|
||||||
|
-- Trigger on UPDATE (only when relevant fields change)
|
||||||
|
CREATE TRIGGER trg_combo_denorm_update
|
||||||
|
BEFORE UPDATE ON lens_shutter_combos
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (
|
||||||
|
OLD.housing_variant_id IS DISTINCT FROM NEW.housing_variant_id OR
|
||||||
|
OLD.lens_id IS DISTINCT FROM NEW.lens_id OR
|
||||||
|
OLD.shutter_id IS DISTINCT FROM NEW.shutter_id
|
||||||
|
)
|
||||||
|
EXECUTE FUNCTION update_combo_denormalized();
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION update_combo_denormalized IS 'Maintains denormalized fields in lens_shutter_combos for query performance';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- FUNCTION: Version Increment & Timestamp
|
||||||
|
-- ==========================================
|
||||||
|
CREATE OR REPLACE FUNCTION update_version_and_timestamp()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.version := OLD.version + 1;
|
||||||
|
NEW.updated_at := NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Apply to main tables
|
||||||
|
CREATE TRIGGER trg_mfg_version
|
||||||
|
BEFORE UPDATE ON manufacturers
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_version_and_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_models_version
|
||||||
|
BEFORE UPDATE ON camera_models
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_version_and_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_variants_version
|
||||||
|
BEFORE UPDATE ON housing_variants
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_version_and_timestamp();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_combos_version
|
||||||
|
BEFORE UPDATE ON lens_shutter_combos
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_version_and_timestamp();
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION update_version_and_timestamp IS 'Increments version number and updates timestamp on every change';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- FUNCTION: Maintain Camera Models History
|
||||||
|
-- ==========================================
|
||||||
|
CREATE OR REPLACE FUNCTION camera_models_to_history()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF TG_OP = 'UPDATE' THEN
|
||||||
|
INSERT INTO camera_models_history (
|
||||||
|
id, manufacturer_id, goergens_model_number, body_type,
|
||||||
|
viewfinder_type, format_code, year_first, year_last,
|
||||||
|
version, changed_at, changed_by, change_type
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
OLD.id, OLD.manufacturer_id, OLD.goergens_model_number, OLD.body_type,
|
||||||
|
OLD.viewfinder_type, OLD.format_code, OLD.year_first, OLD.year_last,
|
||||||
|
OLD.version, NOW(), NEW.updated_by, 'UPDATE'
|
||||||
|
);
|
||||||
|
ELSIF TG_OP = 'DELETE' THEN
|
||||||
|
INSERT INTO camera_models_history (
|
||||||
|
id, manufacturer_id, goergens_model_number, body_type,
|
||||||
|
viewfinder_type, format_code, year_first, year_last,
|
||||||
|
version, changed_at, changed_by, change_type
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
OLD.id, OLD.manufacturer_id, OLD.goergens_model_number, OLD.body_type,
|
||||||
|
OLD.viewfinder_type, OLD.format_code, OLD.year_first, OLD.year_last,
|
||||||
|
OLD.version, NOW(), NULL, 'DELETE'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_models_history
|
||||||
|
AFTER UPDATE OR DELETE ON camera_models
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION camera_models_to_history();
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION camera_models_to_history IS 'Maintains historical versions of camera models';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- FUNCTION: Maintain Combos History
|
||||||
|
-- ==========================================
|
||||||
|
CREATE OR REPLACE FUNCTION lens_shutter_combos_to_history()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF TG_OP = 'UPDATE' THEN
|
||||||
|
INSERT INTO lens_shutter_combos_history (
|
||||||
|
id, housing_variant_id, goergens_combo_number, lens_id, shutter_id,
|
||||||
|
weight_g, width_mm, height_mm, depth_mm, specs,
|
||||||
|
version, changed_at, changed_by, change_type
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
OLD.id, OLD.housing_variant_id, OLD.goergens_combo_number, OLD.lens_id, OLD.shutter_id,
|
||||||
|
OLD.weight_g, OLD.width_mm, OLD.height_mm, OLD.depth_mm, OLD.specs,
|
||||||
|
OLD.version, NOW(), NEW.updated_by, 'UPDATE'
|
||||||
|
);
|
||||||
|
ELSIF TG_OP = 'DELETE' THEN
|
||||||
|
INSERT INTO lens_shutter_combos_history (
|
||||||
|
id, housing_variant_id, goergens_combo_number, lens_id, shutter_id,
|
||||||
|
weight_g, width_mm, height_mm, depth_mm, specs,
|
||||||
|
version, changed_at, changed_by, change_type
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
OLD.id, OLD.housing_variant_id, OLD.goergens_combo_number, OLD.lens_id, OLD.shutter_id,
|
||||||
|
OLD.weight_g, OLD.width_mm, OLD.height_mm, OLD.depth_mm, OLD.specs,
|
||||||
|
OLD.version, NOW(), NULL, 'DELETE'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_combos_history
|
||||||
|
AFTER UPDATE OR DELETE ON lens_shutter_combos
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION lens_shutter_combos_to_history();
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION lens_shutter_combos_to_history IS 'Maintains historical versions of camera combinations';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- FUNCTION: Validate Goergens ID Structure
|
||||||
|
-- ==========================================
|
||||||
|
CREATE OR REPLACE FUNCTION validate_goergens_structure()
|
||||||
|
RETURNS TABLE(combo_id INT, goergens_id VARCHAR, issue TEXT) AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Check model numbers are consistent
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
lsc.id,
|
||||||
|
lsc.goergens_full_id,
|
||||||
|
'Inconsistent model number in same camera_model'::TEXT AS issue
|
||||||
|
FROM lens_shutter_combos lsc
|
||||||
|
JOIN housing_variants hv ON lsc.housing_variant_id = hv.id
|
||||||
|
JOIN camera_models cm ON hv.camera_model_id = cm.id
|
||||||
|
WHERE split_part(lsc.goergens_full_id, ' ', 3) != cm.goergens_model_number;
|
||||||
|
|
||||||
|
-- Check variant letters are consistent
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
lsc.id,
|
||||||
|
lsc.goergens_full_id,
|
||||||
|
'Inconsistent variant letter'::TEXT AS issue
|
||||||
|
FROM lens_shutter_combos lsc
|
||||||
|
JOIN housing_variants hv ON lsc.housing_variant_id = hv.id
|
||||||
|
WHERE substring(split_part(lsc.goergens_full_id, ' ', 4), 1, 1) != hv.goergens_variant_letter;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION validate_goergens_structure IS 'Validates that Goergens IDs match the hierarchy structure';
|
||||||
|
|
@ -0,0 +1,305 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 7: Materialized Views
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- MV: MANUFACTURER STATISTICS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE MATERIALIZED VIEW mv_manufacturer_stats AS
|
||||||
|
SELECT
|
||||||
|
m.id AS manufacturer_id,
|
||||||
|
m.name AS manufacturer_name,
|
||||||
|
m.goergens_code,
|
||||||
|
m.country,
|
||||||
|
|
||||||
|
COUNT(DISTINCT cm.id) AS model_count,
|
||||||
|
COUNT(DISTINCT lsc.id) AS camera_count,
|
||||||
|
|
||||||
|
MIN(cm.year_first) AS first_camera_year,
|
||||||
|
MAX(cm.year_last) AS last_camera_year,
|
||||||
|
|
||||||
|
COUNT(DISTINCT CASE WHEN i.rights_status = 'verified' THEN i.id END) AS verified_image_count,
|
||||||
|
COUNT(DISTINCT CASE WHEN d.document_type = 'user_manual' THEN d.id END) AS manual_count,
|
||||||
|
COUNT(DISTINCT CASE WHEN d.document_type = 'repair_manual' THEN d.id END) AS repair_manual_count,
|
||||||
|
COUNT(DISTINCT ca.article_id) AS article_count
|
||||||
|
|
||||||
|
FROM manufacturers m
|
||||||
|
LEFT JOIN camera_models cm ON m.id = cm.manufacturer_id AND cm.deleted_at IS NULL
|
||||||
|
LEFT JOIN housing_variants hv ON cm.id = hv.camera_model_id AND hv.deleted_at IS NULL
|
||||||
|
LEFT JOIN lens_shutter_combos lsc ON hv.id = lsc.housing_variant_id AND lsc.deleted_at IS NULL
|
||||||
|
LEFT JOIN images i ON (
|
||||||
|
i.manufacturer_id = m.id OR
|
||||||
|
i.camera_model_id = cm.id OR
|
||||||
|
i.housing_variant_id = hv.id OR
|
||||||
|
i.combo_id = lsc.id
|
||||||
|
) AND i.deleted_at IS NULL
|
||||||
|
LEFT JOIN documents d ON (
|
||||||
|
d.manufacturer_id = m.id OR
|
||||||
|
d.camera_model_id = cm.id OR
|
||||||
|
d.housing_variant_id = hv.id OR
|
||||||
|
d.combo_id = lsc.id
|
||||||
|
) AND d.deleted_at IS NULL
|
||||||
|
LEFT JOIN camera_articles ca ON (
|
||||||
|
ca.manufacturer_id = m.id OR
|
||||||
|
ca.camera_model_id = cm.id OR
|
||||||
|
ca.housing_variant_id = hv.id OR
|
||||||
|
ca.combo_id = lsc.id
|
||||||
|
)
|
||||||
|
|
||||||
|
WHERE m.deleted_at IS NULL
|
||||||
|
GROUP BY m.id, m.name, m.goergens_code, m.country;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_mv_mfg_stats_id ON mv_manufacturer_stats(manufacturer_id);
|
||||||
|
CREATE INDEX idx_mv_mfg_stats_name ON mv_manufacturer_stats(manufacturer_name);
|
||||||
|
CREATE INDEX idx_mv_mfg_stats_country ON mv_manufacturer_stats(country);
|
||||||
|
|
||||||
|
COMMENT ON MATERIALIZED VIEW mv_manufacturer_stats IS
|
||||||
|
'Pre-computed manufacturer statistics. Refresh hourly with: REFRESH MATERIALIZED VIEW CONCURRENTLY mv_manufacturer_stats;';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- MV: PUBLIC CATALOG (Fast Search Index)
|
||||||
|
-- ==========================================
|
||||||
|
CREATE MATERIALIZED VIEW mv_public_catalog AS
|
||||||
|
SELECT
|
||||||
|
lsc.id AS combo_id,
|
||||||
|
lsc.goergens_full_id,
|
||||||
|
lsc.display_name,
|
||||||
|
|
||||||
|
m.id AS manufacturer_id,
|
||||||
|
m.name AS manufacturer_name,
|
||||||
|
m.country,
|
||||||
|
|
||||||
|
cm.id AS model_id,
|
||||||
|
i18n.model_name,
|
||||||
|
cm.body_type,
|
||||||
|
cm.viewfinder_type,
|
||||||
|
cm.format_code,
|
||||||
|
cm.year_first,
|
||||||
|
cm.year_last,
|
||||||
|
|
||||||
|
-- Terminology lookups
|
||||||
|
(SELECT term_de FROM terminology WHERE concept_code = 'BODY_TYPE_' || UPPER(cm.body_type)) AS body_type_de,
|
||||||
|
(SELECT term_en FROM terminology WHERE concept_code = 'BODY_TYPE_' || UPPER(cm.body_type)) AS body_type_en,
|
||||||
|
|
||||||
|
-- Lens info
|
||||||
|
l.name AS lens_name,
|
||||||
|
l.focal_length_mm,
|
||||||
|
l.max_aperture,
|
||||||
|
|
||||||
|
-- Shutter info
|
||||||
|
s.name AS shutter_name,
|
||||||
|
|
||||||
|
-- Media flags
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM images
|
||||||
|
WHERE (combo_id = lsc.id OR camera_model_id = cm.id OR housing_variant_id = hv.id OR manufacturer_id = m.id)
|
||||||
|
AND rights_status = 'verified'
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
) AS has_images,
|
||||||
|
|
||||||
|
(
|
||||||
|
SELECT filename FROM images
|
||||||
|
WHERE camera_model_id = cm.id
|
||||||
|
AND rights_status = 'verified'
|
||||||
|
AND sort_order = 0
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
) AS primary_image_filename,
|
||||||
|
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM documents
|
||||||
|
WHERE (combo_id = lsc.id OR camera_model_id = cm.id)
|
||||||
|
AND document_type = 'user_manual'
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
) AS has_manual,
|
||||||
|
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1 FROM documents
|
||||||
|
WHERE (combo_id = lsc.id OR camera_model_id = cm.id)
|
||||||
|
AND document_type = 'repair_manual'
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
) AS has_repair_manual,
|
||||||
|
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM camera_articles
|
||||||
|
WHERE combo_id = lsc.id OR camera_model_id = cm.id
|
||||||
|
) AS article_count,
|
||||||
|
|
||||||
|
-- Search text
|
||||||
|
m.name || ' ' ||
|
||||||
|
COALESCE(i18n.model_name, '') || ' ' ||
|
||||||
|
COALESCE(i18n.description_de, '') || ' ' ||
|
||||||
|
cm.format_code || ' ' ||
|
||||||
|
COALESCE(l.name, '') || ' ' ||
|
||||||
|
COALESCE(s.name, '') AS search_text
|
||||||
|
|
||||||
|
FROM lens_shutter_combos lsc
|
||||||
|
JOIN housing_variants hv ON lsc.housing_variant_id = hv.id
|
||||||
|
JOIN camera_models cm ON hv.camera_model_id = cm.id
|
||||||
|
JOIN manufacturers m ON cm.manufacturer_id = m.id
|
||||||
|
LEFT JOIN camera_model_i18n i18n ON cm.id = i18n.camera_model_id
|
||||||
|
LEFT JOIN lenses l ON lsc.lens_id = l.id
|
||||||
|
LEFT JOIN shutters s ON lsc.shutter_id = s.id
|
||||||
|
|
||||||
|
WHERE lsc.deleted_at IS NULL
|
||||||
|
AND hv.deleted_at IS NULL
|
||||||
|
AND cm.deleted_at IS NULL
|
||||||
|
AND m.deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_mv_catalog_id ON mv_public_catalog(combo_id);
|
||||||
|
CREATE INDEX idx_mv_catalog_mfg ON mv_public_catalog(manufacturer_name);
|
||||||
|
CREATE INDEX idx_mv_catalog_years ON mv_public_catalog(year_first, year_last);
|
||||||
|
CREATE INDEX idx_mv_catalog_body ON mv_public_catalog(body_type);
|
||||||
|
CREATE INDEX idx_mv_catalog_format ON mv_public_catalog(format_code);
|
||||||
|
CREATE INDEX idx_mv_catalog_fts ON mv_public_catalog USING GIN(to_tsvector('german', search_text));
|
||||||
|
|
||||||
|
COMMENT ON MATERIALIZED VIEW mv_public_catalog IS
|
||||||
|
'Pre-joined catalog for fast public search. Refresh every 15-30 minutes.';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- MV: DASHBOARD STATISTICS
|
||||||
|
-- ==========================================
|
||||||
|
CREATE MATERIALIZED VIEW mv_dashboard_stats AS
|
||||||
|
SELECT
|
||||||
|
'total_cameras' AS metric,
|
||||||
|
COUNT(*)::TEXT AS value,
|
||||||
|
NOW() AS calculated_at
|
||||||
|
FROM lens_shutter_combos WHERE deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'total_manufacturers', COUNT(*)::TEXT, NOW()
|
||||||
|
FROM manufacturers WHERE deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'total_models', COUNT(*)::TEXT, NOW()
|
||||||
|
FROM camera_models WHERE deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'cameras_with_images', COUNT(DISTINCT combo_id)::TEXT, NOW()
|
||||||
|
FROM images WHERE rights_status = 'verified' AND deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'cameras_need_images',
|
||||||
|
(SELECT COUNT(*) FROM lens_shutter_combos lsc
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND NOT EXISTS(
|
||||||
|
SELECT 1 FROM images
|
||||||
|
WHERE (combo_id = lsc.id OR camera_model_id = lsc.camera_model_id)
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
))::TEXT,
|
||||||
|
NOW()
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'uncertain_images', COUNT(*)::TEXT, NOW()
|
||||||
|
FROM images WHERE rights_status = 'uncertain' AND deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'total_articles', COUNT(*)::TEXT, NOW()
|
||||||
|
FROM articles WHERE deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'total_documents', COUNT(*)::TEXT, NOW()
|
||||||
|
FROM documents WHERE deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'user_manuals', COUNT(*)::TEXT, NOW()
|
||||||
|
FROM documents WHERE document_type = 'user_manual' AND deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'repair_manuals', COUNT(*)::TEXT, NOW()
|
||||||
|
FROM documents WHERE document_type = 'repair_manual' AND deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'newest_camera_year', MAX(year_last)::TEXT, NOW()
|
||||||
|
FROM camera_models WHERE deleted_at IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT 'oldest_camera_year', MIN(year_first)::TEXT, NOW()
|
||||||
|
FROM camera_models WHERE deleted_at IS NULL AND year_first IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX idx_mv_dashboard_metric ON mv_dashboard_stats(metric);
|
||||||
|
|
||||||
|
COMMENT ON MATERIALIZED VIEW mv_dashboard_stats IS
|
||||||
|
'Admin dashboard metrics. Refresh every 5 minutes.';
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CONVENIENCE VIEW: Complete Camera Info
|
||||||
|
-- ==========================================
|
||||||
|
CREATE VIEW v_cameras_complete AS
|
||||||
|
SELECT
|
||||||
|
lsc.id AS combo_id,
|
||||||
|
lsc.goergens_full_id,
|
||||||
|
lsc.display_name,
|
||||||
|
|
||||||
|
-- Manufacturer
|
||||||
|
m.id AS manufacturer_id,
|
||||||
|
m.name AS manufacturer_name,
|
||||||
|
m.goergens_code AS manufacturer_code,
|
||||||
|
m.country,
|
||||||
|
|
||||||
|
-- Model
|
||||||
|
cm.id AS model_id,
|
||||||
|
i18n.model_name,
|
||||||
|
cm.body_type,
|
||||||
|
cm.viewfinder_type,
|
||||||
|
cm.format_code,
|
||||||
|
cm.year_first,
|
||||||
|
cm.year_last,
|
||||||
|
i18n.description_de,
|
||||||
|
i18n.description_en,
|
||||||
|
|
||||||
|
-- Variant
|
||||||
|
hv.id AS variant_id,
|
||||||
|
hv.goergens_variant_letter,
|
||||||
|
hv.color,
|
||||||
|
hv.body_material,
|
||||||
|
|
||||||
|
-- Combo specifics
|
||||||
|
lsc.goergens_combo_number,
|
||||||
|
|
||||||
|
-- Lens
|
||||||
|
l.id AS lens_id,
|
||||||
|
l.name AS lens_name,
|
||||||
|
l.focal_length_mm,
|
||||||
|
l.max_aperture,
|
||||||
|
|
||||||
|
-- Shutter
|
||||||
|
s.id AS shutter_id,
|
||||||
|
s.name AS shutter_name,
|
||||||
|
s.shutter_type,
|
||||||
|
|
||||||
|
-- Technical
|
||||||
|
lsc.weight_g,
|
||||||
|
lsc.width_mm,
|
||||||
|
lsc.height_mm,
|
||||||
|
lsc.depth_mm,
|
||||||
|
lsc.specs,
|
||||||
|
|
||||||
|
-- Audit
|
||||||
|
lsc.created_at,
|
||||||
|
lsc.updated_at,
|
||||||
|
lsc.version
|
||||||
|
|
||||||
|
FROM lens_shutter_combos lsc
|
||||||
|
JOIN housing_variants hv ON lsc.housing_variant_id = hv.id
|
||||||
|
JOIN camera_models cm ON hv.camera_model_id = cm.id
|
||||||
|
JOIN manufacturers m ON cm.manufacturer_id = m.id
|
||||||
|
LEFT JOIN camera_model_i18n i18n ON cm.id = i18n.camera_model_id
|
||||||
|
LEFT JOIN lenses l ON lsc.lens_id = l.id
|
||||||
|
LEFT JOIN shutters s ON lsc.shutter_id = s.id
|
||||||
|
|
||||||
|
WHERE lsc.deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMENT ON VIEW v_cameras_complete IS 'Complete camera information with all hierarchy levels joined';
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
-- ==========================================
|
||||||
|
-- CLUB DAGUERRE CAMERA DATABASE
|
||||||
|
-- Part 8: Seed Data
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CONTENT SOURCES
|
||||||
|
-- ==========================================
|
||||||
|
INSERT INTO content_sources (source_name, source_type, rights_status, legal_notes, verified_by, verified_date) VALUES
|
||||||
|
('Club Daguerre - Photo Antiquaria', 'internal', 'owned', 'Club owns publication and digitization rights', 'Walter Jekat', CURRENT_DATE),
|
||||||
|
('Club Daguerre aktuell', 'internal', 'owned', 'Club publication, full rights', 'Walter Jekat', CURRENT_DATE),
|
||||||
|
('Harald Goergens Collection', 'licensed', 'owned', 'Harald grants perpetual license for his photos/data to Club Daguerre', 'Walter Jekat', CURRENT_DATE),
|
||||||
|
('Steimer Database - Images', 'uncertain', 'questionable', 'Source unknown. DO NOT publish without clearance. Internal research only.', NULL, NULL),
|
||||||
|
('Steimer Database - Text', 'uncertain', 'questionable', 'Text from various publications. Rights status unknown. Use metadata only.', NULL, NULL),
|
||||||
|
('User Contributed', 'licensed', 'licensed', 'Users grant CC-BY-SA license on upload', 'System', CURRENT_DATE),
|
||||||
|
('Pre-1923 Public Domain', 'public_domain', 'public_domain', 'Works published before 1923 are public domain', 'Walter Jekat', CURRENT_DATE),
|
||||||
|
('Manufacturer Marketing', 'public_domain', 'fair_use', 'Brochures, ads, price lists - marketing materials (fair use)', 'Walter Jekat', CURRENT_DATE);
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- CORE TERMINOLOGY CONCEPTS
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
-- Body Types
|
||||||
|
INSERT INTO concepts (concept_code, category, sort_order) VALUES
|
||||||
|
('BODY_TYPE_LB', 'body_type', 1),
|
||||||
|
('BODY_TYPE_FG', 'body_type', 2),
|
||||||
|
('BODY_TYPE_KL', 'body_type', 3),
|
||||||
|
('BODY_TYPE_SP', 'body_type', 4),
|
||||||
|
('BODY_TYPE_RK', 'body_type', 5),
|
||||||
|
('BODY_TYPE_BO', 'body_type', 6),
|
||||||
|
('BODY_TYPE_TU', 'body_type', 7),
|
||||||
|
('BODY_TYPE_MG', 'body_type', 8);
|
||||||
|
|
||||||
|
-- Body Type Terms
|
||||||
|
INSERT INTO concept_terms (concept_id, language_code, term, term_type) VALUES
|
||||||
|
-- Laufboden (Lb)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_LB'), 'de', 'Laufbodenkamera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_LB'), 'de', 'Lb', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_LB'), 'de', 'Laufboden', 'synonym'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_LB'), 'en', 'Strut folder camera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_LB'), 'en', 'SF', 'abbreviation'),
|
||||||
|
|
||||||
|
-- Festgehäuse (Fg)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_FG'), 'de', 'Festgehäuse', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_FG'), 'de', 'Fg', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_FG'), 'en', 'Rigid body camera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_FG'), 'en', 'Rigid', 'synonym'),
|
||||||
|
|
||||||
|
-- Klappkamera (Kl)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_KL'), 'de', 'Klappkamera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_KL'), 'de', 'Kl', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_KL'), 'en', 'Folding camera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_KL'), 'en', 'Folder', 'synonym'),
|
||||||
|
|
||||||
|
-- Spiegelreflex (Sp)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_SP'), 'de', 'Spiegelreflexkamera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_SP'), 'de', 'Sp', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_SP'), 'de', 'Spiegelreflex', 'synonym'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_SP'), 'en', 'Single-lens reflex', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_SP'), 'en', 'SLR', 'abbreviation'),
|
||||||
|
|
||||||
|
-- Reflexkamera (Rk)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_RK'), 'de', 'Reflexkamera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_RK'), 'de', 'Rk', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_RK'), 'en', 'Twin-lens reflex', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_RK'), 'en', 'TLR', 'abbreviation'),
|
||||||
|
|
||||||
|
-- Boxkamera (Bo)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_BO'), 'de', 'Boxkamera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_BO'), 'de', 'Bo', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_BO'), 'de', 'Box', 'synonym'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_BO'), 'en', 'Box camera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_BO'), 'en', 'Box', 'abbreviation'),
|
||||||
|
|
||||||
|
-- Tubus (Tu)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_TU'), 'de', 'Tubuskamera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_TU'), 'de', 'Tu', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_TU'), 'en', 'Tube camera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_TU'), 'en', 'Tube', 'abbreviation'),
|
||||||
|
|
||||||
|
-- Magazine (Mg)
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_MG'), 'de', 'Magazinkamera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_MG'), 'de', 'Mg', 'abbreviation'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_MG'), 'en', 'Magazine camera', 'primary'),
|
||||||
|
((SELECT id FROM concepts WHERE concept_code = 'BODY_TYPE_MG'), 'en', 'Mag', 'abbreviation');
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- COMMON FILM FORMATS
|
||||||
|
-- ==========================================
|
||||||
|
INSERT INTO formats (format_code, width_mm, height_mm, format_type, description_de, description_en, sort_order) VALUES
|
||||||
|
('9x12 Pl', 90, 120, 'plate', 'Glasplatte 9×12 cm', 'Glass plate 9×12 cm', 10),
|
||||||
|
('13x18 Pl', 130, 180, 'plate', 'Glasplatte 13×18 cm', 'Glass plate 13×18 cm', 20),
|
||||||
|
('6x9 RF', 60, 90, 'rollfilm', 'Rollfilm 6×9 cm', 'Rollfilm 6×9 cm', 30),
|
||||||
|
('6x6 RF', 60, 60, 'rollfilm', 'Rollfilm 6×6 cm', 'Rollfilm 6×6 cm', 40),
|
||||||
|
('24x36 KB', 24, 36, '35mm', 'Kleinbild 24×36 mm', '35mm film', 50);
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- SAMPLE LENSES
|
||||||
|
-- ==========================================
|
||||||
|
INSERT INTO lenses (name, manufacturer, focal_length_mm, max_aperture, lens_type, notes) VALUES
|
||||||
|
('Tessar', 'Zeiss', 50, 3.5, 'prime', 'Classic 4-element lens'),
|
||||||
|
('Tessar', 'Zeiss', 105, 4.5, 'prime', 'Medium telephoto version'),
|
||||||
|
('Xenar', 'Schneider', 50, 2.8, 'prime', 'High-speed Tessar type'),
|
||||||
|
('Anastigmat', 'Various', NULL, NULL, 'prime', 'Generic designation for corrected lenses');
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- SAMPLE SHUTTERS
|
||||||
|
-- ==========================================
|
||||||
|
INSERT INTO shutters (name, manufacturer, shutter_type, speed_range, notes) VALUES
|
||||||
|
('Compur', 'Deckel', 'leaf', '1-1/300s, B', 'Most common leaf shutter'),
|
||||||
|
('Prontor', 'Gauthier', 'leaf', '1-1/300s, B', 'Alternative to Compur'),
|
||||||
|
('Ibsor', 'Deckel', 'leaf', '1-1/100s, B', 'Budget version of Compur');
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- SYSTEM CONFIGURATION
|
||||||
|
-- ==========================================
|
||||||
|
INSERT INTO system_config (key, value, description, updated_by, updated_at) VALUES
|
||||||
|
('schema_version', '1.0', 'Current database schema version', 'Installation', NOW()),
|
||||||
|
('last_import', NULL, 'Timestamp of last Goergens data import', NULL, NULL),
|
||||||
|
('research_password_changed', NULL, 'Date of last research password change', NULL, NULL);
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- ANALYSIS REPORT
|
||||||
|
-- ==========================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
table_count INT;
|
||||||
|
index_count INT;
|
||||||
|
view_count INT;
|
||||||
|
function_count INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO table_count
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public' AND table_type = 'BASE TABLE';
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO index_count
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE schemaname = 'public';
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO view_count
|
||||||
|
FROM information_schema.views
|
||||||
|
WHERE table_schema = 'public';
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO function_count
|
||||||
|
FROM pg_proc
|
||||||
|
WHERE pronamespace = 'public'::regnamespace;
|
||||||
|
|
||||||
|
RAISE NOTICE '===========================================';
|
||||||
|
RAISE NOTICE 'CLUB DAGUERRE CAMERA DATABASE';
|
||||||
|
RAISE NOTICE 'Schema Installation Complete';
|
||||||
|
RAISE NOTICE '===========================================';
|
||||||
|
RAISE NOTICE 'Tables created: %', table_count;
|
||||||
|
RAISE NOTICE 'Indexes created: %', index_count;
|
||||||
|
RAISE NOTICE 'Views created: %', view_count;
|
||||||
|
RAISE NOTICE 'Functions created: %', function_count;
|
||||||
|
RAISE NOTICE '===========================================';
|
||||||
|
RAISE NOTICE 'Ready for Goergens data import';
|
||||||
|
RAISE NOTICE '===========================================';
|
||||||
|
END $$;
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
# Club Daguerre Camera Database - Schema Installation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Complete PostgreSQL 16 schema for the Club Daguerre camera database, preserving Harald Goergens' 30+ year classification work with modern metadata management.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- PostgreSQL 16 installed
|
||||||
|
- camera_db database created
|
||||||
|
- Port 5433 (or your configured port)
|
||||||
|
|
||||||
|
## Installation Order
|
||||||
|
|
||||||
|
The schema is split into logical sections for clarity and maintainability:
|
||||||
|
|
||||||
|
1. **01-core-hierarchy.sql** - Manufacturers, models, variants, combos
|
||||||
|
2. **02-supporting.sql** - Lenses, shutters, formats, terminology
|
||||||
|
3. **03-content.sql** - Images, documents, articles (multi-level linking)
|
||||||
|
4. **04-auth.sql** - Members, research access, audit log
|
||||||
|
5. **05-history.sql** - History tables for audit trail
|
||||||
|
6. **06-triggers.sql** - Triggers and functions for automation
|
||||||
|
7. **07-views.sql** - Materialized views for performance
|
||||||
|
8. **08-seed-data.sql** - Initial data (sources, terminology, samples)
|
||||||
|
|
||||||
|
## Quick Installation
|
||||||
|
|
||||||
|
### Method 1: All at Once
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/camera-database/schema
|
||||||
|
|
||||||
|
# Run all files in order
|
||||||
|
for file in 0{1..8}-*.sql; do
|
||||||
|
echo "Installing $file..."
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f "$file"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: One by One
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/camera-database/schema
|
||||||
|
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 01-core-hierarchy.sql
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 02-supporting.sql
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 03-content.sql
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 04-auth.sql
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 05-history.sql
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 06-triggers.sql
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 07-views.sql
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db -f 08-seed-data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After installation:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Connect to database
|
||||||
|
psql -h localhost -p 5433 -U postgres -d camera_db
|
||||||
|
|
||||||
|
-- Check tables
|
||||||
|
\dt
|
||||||
|
|
||||||
|
-- Check views
|
||||||
|
\dv
|
||||||
|
|
||||||
|
-- Check materialized views
|
||||||
|
\dm
|
||||||
|
|
||||||
|
-- Check functions
|
||||||
|
\df
|
||||||
|
|
||||||
|
-- Run validation
|
||||||
|
SELECT * FROM validate_goergens_structure();
|
||||||
|
|
||||||
|
-- Check seed data
|
||||||
|
SELECT * FROM content_sources;
|
||||||
|
SELECT * FROM terminology LIMIT 10;
|
||||||
|
|
||||||
|
-- Exit
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Statistics
|
||||||
|
|
||||||
|
Expected after installation:
|
||||||
|
- **Tables:** 35
|
||||||
|
- **Indexes:** ~80
|
||||||
|
- **Views:** 2
|
||||||
|
- **Materialized Views:** 3
|
||||||
|
- **Functions:** 5
|
||||||
|
- **Triggers:** 8
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### 1. Goergens Hierarchy Preservation
|
||||||
|
- Manufacturers (ERNM, ZEII, etc.)
|
||||||
|
- Models (1910, 0450, etc.)
|
||||||
|
- Variants (a, b, c - color/material)
|
||||||
|
- Combos (01, 02 - lens/shutter configs)
|
||||||
|
|
||||||
|
### 2. Multi-Level Content Linking
|
||||||
|
Images, documents, and articles can link at ANY level:
|
||||||
|
- Manufacturer level (company history)
|
||||||
|
- Model level (camera manual - shared by all variants)
|
||||||
|
- Variant level (color-specific photo)
|
||||||
|
- Combo level (lens manual - combo-specific)
|
||||||
|
|
||||||
|
### 3. Rights Management
|
||||||
|
- **verified:** Publishable content
|
||||||
|
- **uncertain:** Research-only (Steimer data)
|
||||||
|
- **restricted:** Internal only
|
||||||
|
|
||||||
|
### 4. Performance Optimizations
|
||||||
|
- Denormalized fields in lens_shutter_combos
|
||||||
|
- Materialized views for dashboard/search
|
||||||
|
- GIN indexes for full-text search
|
||||||
|
- GIST indexes for range queries
|
||||||
|
|
||||||
|
### 5. Audit Trail
|
||||||
|
- Version numbers on all records
|
||||||
|
- History tables track all changes
|
||||||
|
- Comprehensive audit log
|
||||||
|
- Timestamp tracking
|
||||||
|
|
||||||
|
### 6. Multilingual Support
|
||||||
|
- German/English core (expandable)
|
||||||
|
- Separate i18n tables for content
|
||||||
|
- Language-agnostic codes
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Refresh Materialized Views
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Hourly (manufacturer stats)
|
||||||
|
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_manufacturer_stats;
|
||||||
|
|
||||||
|
-- Every 15-30 minutes (search catalog)
|
||||||
|
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_public_catalog;
|
||||||
|
|
||||||
|
-- Every 5 minutes (dashboard)
|
||||||
|
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dashboard_stats;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full backup
|
||||||
|
pg_dump -h localhost -p 5433 -U postgres camera_db | gzip > /mnt/data/postgresql/backups/camera_db_$(date +%Y%m%d).sql.gz
|
||||||
|
|
||||||
|
# Schema only
|
||||||
|
pg_dump -h localhost -p 5433 -U postgres -s camera_db > /mnt/data/postgresql/backups/camera_db_schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Schema installed
|
||||||
|
2. Import Goergens data (74,000+ cameras)
|
||||||
|
3. Import Photo Antiquaria articles
|
||||||
|
4. Add images and documents
|
||||||
|
5. Develop Flask web application
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "relation already exists"
|
||||||
|
Schema already partially installed. Either:
|
||||||
|
- Drop database and recreate: `DROP DATABASE camera_db; CREATE DATABASE camera_db;`
|
||||||
|
- Or skip to the file where installation failed
|
||||||
|
|
||||||
|
### Foreign key violations
|
||||||
|
Files must be run in order (01 through 08). Foreign keys depend on tables from earlier files.
|
||||||
|
|
||||||
|
### Permission denied
|
||||||
|
Use postgres superuser: `-U postgres`
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **Design Document:** /data/camera-database/docs/database-design.md
|
||||||
|
- **PostgreSQL Setup:** /data/camera-database/docs/postgresql-setup.md
|
||||||
|
- **Goergens Email:** /data/camera-database/docs/goergens-email.txt
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
- Schema Version: 1.0
|
||||||
|
- PostgreSQL: 16+
|
||||||
|
- Date: November 2025
|
||||||
|
- Author: Walter Jekat / Club Daguerre e.V.
|
||||||
Loading…
Reference in New Issue