PlotDirector/PlotLine/Sql/148_Phase23_NameIntelligence.sql

633 lines
25 KiB
Transact-SQL

SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.NameLocales', N'U') IS NULL
BEGIN
CREATE TABLE dbo.NameLocales
(
NameLocaleID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameLocales PRIMARY KEY,
LocaleCode nvarchar(16) NOT NULL,
DisplayName nvarchar(100) NOT NULL,
IsoCountryCode nvarchar(3) NOT NULL,
EvidenceBasis nvarchar(300) NOT NULL,
DefaultConfidence decimal(9,4) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_NameLocales_IsActive DEFAULT 1,
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameLocales_CreatedUtc DEFAULT SYSUTCDATETIME(),
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameLocales_UpdatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT UQ_NameLocales_LocaleCode UNIQUE (LocaleCode),
CONSTRAINT CK_NameLocales_DefaultConfidence CHECK (DefaultConfidence >= 0 AND DefaultConfidence <= 1)
);
END;
GO
IF OBJECT_ID(N'dbo.GivenNames', N'U') IS NULL
BEGIN
CREATE TABLE dbo.GivenNames
(
GivenNameID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_GivenNames PRIMARY KEY,
GivenNameKey nvarchar(80) NOT NULL,
DisplayName nvarchar(120) NOT NULL,
NormalisedName nvarchar(120) COLLATE Latin1_General_100_CI_AI_SC NOT NULL,
NameType nvarchar(40) NOT NULL,
IsWordAmbiguous bit NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_GivenNames_IsActive DEFAULT 1,
Notes nvarchar(500) NULL,
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_GivenNames_CreatedUtc DEFAULT SYSUTCDATETIME(),
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_GivenNames_UpdatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT UQ_GivenNames_GivenNameKey UNIQUE (GivenNameKey),
CONSTRAINT UQ_GivenNames_NormalisedName UNIQUE (NormalisedName),
CONSTRAINT CK_GivenNames_NameType CHECK (NameType IN (N'Canonical', N'Independent', N'Diminutive', N'Nickname', N'Variant'))
);
END;
GO
IF OBJECT_ID(N'dbo.NameRelationships', N'U') IS NULL
BEGIN
CREATE TABLE dbo.NameRelationships
(
NameRelationshipID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameRelationships PRIMARY KEY,
RelationshipKey nvarchar(80) NOT NULL,
FromGivenNameID int NOT NULL,
ToGivenNameID int NOT NULL,
RelationshipType nvarchar(40) NOT NULL,
Strength decimal(9,4) NOT NULL,
IsBidirectional bit NOT NULL,
IsActive bit NOT NULL CONSTRAINT DF_NameRelationships_IsActive DEFAULT 1,
CurationSource nvarchar(120) NULL,
Notes nvarchar(500) NULL,
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameRelationships_CreatedUtc DEFAULT SYSUTCDATETIME(),
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameRelationships_UpdatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT UQ_NameRelationships_RelationshipKey UNIQUE (RelationshipKey),
CONSTRAINT FK_NameRelationships_FromGivenName FOREIGN KEY (FromGivenNameID) REFERENCES dbo.GivenNames(GivenNameID),
CONSTRAINT FK_NameRelationships_ToGivenName FOREIGN KEY (ToGivenNameID) REFERENCES dbo.GivenNames(GivenNameID),
CONSTRAINT CK_NameRelationships_NotSelf CHECK (FromGivenNameID <> ToGivenNameID),
CONSTRAINT CK_NameRelationships_Strength CHECK (Strength >= 0 AND Strength <= 1),
CONSTRAINT CK_NameRelationships_Type CHECK (RelationshipType IN (N'ShortForm', N'Diminutive', N'Nickname', N'SpellingVariant', N'AlternativeForm', N'AnglicisedForm'))
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_NameRelationships_ActivePairType' AND object_id = OBJECT_ID(N'dbo.NameRelationships'))
CREATE UNIQUE INDEX UX_NameRelationships_ActivePairType ON dbo.NameRelationships(FromGivenNameID, ToGivenNameID, RelationshipType) WHERE IsActive = 1;
GO
IF OBJECT_ID(N'dbo.NameUsages', N'U') IS NULL
BEGIN
CREATE TABLE dbo.NameUsages
(
NameUsageID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameUsages PRIMARY KEY,
UsageKey nvarchar(80) NOT NULL,
GivenNameID int NOT NULL,
NameLocaleID int NOT NULL,
BirthYearFrom int NOT NULL,
BirthYearTo int NOT NULL,
MaleWeight decimal(9,4) NOT NULL,
FemaleWeight decimal(9,4) NOT NULL,
UsageWeight decimal(9,4) NOT NULL,
Confidence decimal(9,4) NOT NULL,
SourceType nvarchar(60) NOT NULL,
SourceReference nvarchar(300) NULL,
IsActive bit NOT NULL CONSTRAINT DF_NameUsages_IsActive DEFAULT 1,
Notes nvarchar(500) NULL,
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameUsages_CreatedUtc DEFAULT SYSUTCDATETIME(),
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameUsages_UpdatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT UQ_NameUsages_UsageKey UNIQUE (UsageKey),
CONSTRAINT UQ_NameUsages_NameLocaleYears UNIQUE (GivenNameID, NameLocaleID, BirthYearFrom, BirthYearTo),
CONSTRAINT FK_NameUsages_GivenName FOREIGN KEY (GivenNameID) REFERENCES dbo.GivenNames(GivenNameID),
CONSTRAINT FK_NameUsages_NameLocale FOREIGN KEY (NameLocaleID) REFERENCES dbo.NameLocales(NameLocaleID),
CONSTRAINT CK_NameUsages_YearOrder CHECK (BirthYearFrom <= BirthYearTo),
CONSTRAINT CK_NameUsages_Weights CHECK (MaleWeight >= 0 AND MaleWeight <= 1 AND FemaleWeight >= 0 AND FemaleWeight <= 1 AND UsageWeight >= 0 AND UsageWeight <= 1 AND Confidence >= 0 AND Confidence <= 1),
CONSTRAINT CK_NameUsages_GenderTotal CHECK (ABS((MaleWeight + FemaleWeight) - 1.0000) <= 0.0002),
CONSTRAINT CK_NameUsages_SourceType CHECK (SourceType IN (N'OfficialStatisticalDerived', N'OfficialSourceBlendedEstimate'))
);
END;
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_NameUsages_QueryPath' AND object_id = OBJECT_ID(N'dbo.NameUsages'))
CREATE INDEX IX_NameUsages_QueryPath ON dbo.NameUsages(GivenNameID, NameLocaleID, IsActive, BirthYearFrom, BirthYearTo) INCLUDE (MaleWeight, FemaleWeight, UsageWeight, Confidence, SourceType);
GO
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_GivenNames_Search' AND object_id = OBJECT_ID(N'dbo.GivenNames'))
CREATE INDEX IX_GivenNames_Search ON dbo.GivenNames(IsActive, NormalisedName, DisplayName) INCLUDE (NameType, IsWordAmbiguous);
GO
IF OBJECT_ID(N'dbo.NameReferenceImports', N'U') IS NULL
BEGIN
CREATE TABLE dbo.NameReferenceImports
(
NameReferenceImportID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameReferenceImports PRIMARY KEY,
PackageID nvarchar(120) NOT NULL,
SemanticVersion nvarchar(40) NOT NULL,
PackageChecksum nvarchar(128) NULL,
StartedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameReferenceImports_StartedUtc DEFAULT SYSUTCDATETIME(),
CompletedUtc datetime2(0) NULL,
Succeeded bit NOT NULL CONSTRAINT DF_NameReferenceImports_Succeeded DEFAULT 0,
LocaleRows int NOT NULL CONSTRAINT DF_NameReferenceImports_LocaleRows DEFAULT 0,
GivenNameRows int NOT NULL CONSTRAINT DF_NameReferenceImports_GivenNameRows DEFAULT 0,
RelationshipRows int NOT NULL CONSTRAINT DF_NameReferenceImports_RelationshipRows DEFAULT 0,
UsageRows int NOT NULL CONSTRAINT DF_NameReferenceImports_UsageRows DEFAULT 0,
ErrorSummary nvarchar(max) NULL
);
END;
GO
IF TYPE_ID(N'dbo.NameLocaleImportTable') IS NULL
CREATE TYPE dbo.NameLocaleImportTable AS TABLE
(
LocaleCode nvarchar(16) NOT NULL,
DisplayName nvarchar(100) NOT NULL,
IsoCountryCode nvarchar(3) NOT NULL,
EvidenceBasis nvarchar(300) NOT NULL,
DefaultConfidence decimal(9,4) NOT NULL,
SortOrder int NOT NULL,
IsActive bit NOT NULL
);
GO
IF TYPE_ID(N'dbo.GivenNameImportTable') IS NULL
CREATE TYPE dbo.GivenNameImportTable AS TABLE
(
GivenNameKey nvarchar(80) NOT NULL,
DisplayName nvarchar(120) NOT NULL,
NormalisedName nvarchar(120) NOT NULL,
NameType nvarchar(40) NOT NULL,
IsWordAmbiguous bit NOT NULL,
IsActive bit NOT NULL,
Notes nvarchar(500) NULL
);
GO
IF TYPE_ID(N'dbo.NameRelationshipImportTable') IS NULL
CREATE TYPE dbo.NameRelationshipImportTable AS TABLE
(
RelationshipKey nvarchar(80) NOT NULL,
FromGivenNameKey nvarchar(80) NOT NULL,
ToGivenNameKey nvarchar(80) NOT NULL,
RelationshipType nvarchar(40) NOT NULL,
Strength decimal(9,4) NOT NULL,
IsBidirectional bit NOT NULL,
IsActive bit NOT NULL,
CurationSource nvarchar(120) NULL,
Notes nvarchar(500) NULL
);
GO
IF TYPE_ID(N'dbo.NameUsageImportTable') IS NULL
CREATE TYPE dbo.NameUsageImportTable AS TABLE
(
UsageKey nvarchar(80) NOT NULL,
GivenNameKey nvarchar(80) NOT NULL,
LocaleCode nvarchar(16) NOT NULL,
BirthYearFrom int NOT NULL,
BirthYearTo int NOT NULL,
MaleWeight decimal(9,4) NOT NULL,
FemaleWeight decimal(9,4) NOT NULL,
UsageWeight decimal(9,4) NOT NULL,
Confidence decimal(9,4) NOT NULL,
SourceType nvarchar(60) NOT NULL,
SourceReference nvarchar(300) NULL,
IsActive bit NOT NULL,
Notes nvarchar(500) NULL
);
GO
CREATE OR ALTER PROCEDURE dbo.NameReferenceImport_GetLatest
@PackageID nvarchar(120),
@SemanticVersion nvarchar(40)
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (1) *
FROM dbo.NameReferenceImports
WHERE PackageID = @PackageID
AND SemanticVersion = @SemanticVersion
AND Succeeded = 1
ORDER BY CompletedUtc DESC, NameReferenceImportID DESC;
END;
GO
CREATE OR ALTER PROCEDURE dbo.NameReferenceImport_Start
@PackageID nvarchar(120),
@SemanticVersion nvarchar(40),
@PackageChecksum nvarchar(128)
AS
BEGIN
SET NOCOUNT ON;
INSERT dbo.NameReferenceImports (PackageID, SemanticVersion, PackageChecksum)
VALUES (@PackageID, @SemanticVersion, @PackageChecksum);
SELECT CAST(SCOPE_IDENTITY() AS int) AS NameReferenceImportID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.NameReferenceImport_Complete
@NameReferenceImportID int,
@Succeeded bit,
@LocaleRows int,
@GivenNameRows int,
@RelationshipRows int,
@UsageRows int,
@ErrorSummary nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
UPDATE dbo.NameReferenceImports
SET CompletedUtc = SYSUTCDATETIME(),
Succeeded = @Succeeded,
LocaleRows = @LocaleRows,
GivenNameRows = @GivenNameRows,
RelationshipRows = @RelationshipRows,
UsageRows = @UsageRows,
ErrorSummary = @ErrorSummary
WHERE NameReferenceImportID = @NameReferenceImportID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.NameLocale_BulkUpsert
@Rows dbo.NameLocaleImportTable READONLY
AS
BEGIN
SET NOCOUNT ON;
MERGE dbo.NameLocales WITH (HOLDLOCK) AS target
USING @Rows AS source
ON target.LocaleCode = source.LocaleCode
WHEN MATCHED THEN
UPDATE SET DisplayName = source.DisplayName,
IsoCountryCode = source.IsoCountryCode,
EvidenceBasis = source.EvidenceBasis,
DefaultConfidence = source.DefaultConfidence,
SortOrder = source.SortOrder,
IsActive = source.IsActive,
UpdatedUtc = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (LocaleCode, DisplayName, IsoCountryCode, EvidenceBasis, DefaultConfidence, SortOrder, IsActive)
VALUES (source.LocaleCode, source.DisplayName, source.IsoCountryCode, source.EvidenceBasis, source.DefaultConfidence, source.SortOrder, source.IsActive);
END;
GO
CREATE OR ALTER PROCEDURE dbo.GivenName_BulkUpsert
@Rows dbo.GivenNameImportTable READONLY
AS
BEGIN
SET NOCOUNT ON;
MERGE dbo.GivenNames WITH (HOLDLOCK) AS target
USING @Rows AS source
ON target.GivenNameKey = source.GivenNameKey
WHEN MATCHED THEN
UPDATE SET DisplayName = source.DisplayName,
NormalisedName = source.NormalisedName,
NameType = source.NameType,
IsWordAmbiguous = source.IsWordAmbiguous,
IsActive = source.IsActive,
Notes = source.Notes,
UpdatedUtc = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (GivenNameKey, DisplayName, NormalisedName, NameType, IsWordAmbiguous, IsActive, Notes)
VALUES (source.GivenNameKey, source.DisplayName, source.NormalisedName, source.NameType, source.IsWordAmbiguous, source.IsActive, source.Notes);
END;
GO
CREATE OR ALTER PROCEDURE dbo.NameRelationship_BulkUpsert
@Rows dbo.NameRelationshipImportTable READONLY
AS
BEGIN
SET NOCOUNT ON;
MERGE dbo.NameRelationships WITH (HOLDLOCK) AS target
USING
(
SELECT r.*, f.GivenNameID AS FromGivenNameID, t.GivenNameID AS ToGivenNameID
FROM @Rows r
INNER JOIN dbo.GivenNames f ON f.GivenNameKey = r.FromGivenNameKey
INNER JOIN dbo.GivenNames t ON t.GivenNameKey = r.ToGivenNameKey
) AS source
ON target.RelationshipKey = source.RelationshipKey
WHEN MATCHED THEN
UPDATE SET FromGivenNameID = source.FromGivenNameID,
ToGivenNameID = source.ToGivenNameID,
RelationshipType = source.RelationshipType,
Strength = source.Strength,
IsBidirectional = source.IsBidirectional,
IsActive = source.IsActive,
CurationSource = source.CurationSource,
Notes = source.Notes,
UpdatedUtc = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (RelationshipKey, FromGivenNameID, ToGivenNameID, RelationshipType, Strength, IsBidirectional, IsActive, CurationSource, Notes)
VALUES (source.RelationshipKey, source.FromGivenNameID, source.ToGivenNameID, source.RelationshipType, source.Strength, source.IsBidirectional, source.IsActive, source.CurationSource, source.Notes);
END;
GO
CREATE OR ALTER PROCEDURE dbo.NameUsage_BulkUpsert
@Rows dbo.NameUsageImportTable READONLY
AS
BEGIN
SET NOCOUNT ON;
MERGE dbo.NameUsages WITH (HOLDLOCK) AS target
USING
(
SELECT r.*, g.GivenNameID, l.NameLocaleID
FROM @Rows r
INNER JOIN dbo.GivenNames g ON g.GivenNameKey = r.GivenNameKey
INNER JOIN dbo.NameLocales l ON l.LocaleCode = r.LocaleCode
) AS source
ON target.UsageKey = source.UsageKey
WHEN MATCHED THEN
UPDATE SET GivenNameID = source.GivenNameID,
NameLocaleID = source.NameLocaleID,
BirthYearFrom = source.BirthYearFrom,
BirthYearTo = source.BirthYearTo,
MaleWeight = source.MaleWeight,
FemaleWeight = source.FemaleWeight,
UsageWeight = source.UsageWeight,
Confidence = source.Confidence,
SourceType = source.SourceType,
SourceReference = source.SourceReference,
IsActive = source.IsActive,
Notes = source.Notes,
UpdatedUtc = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (UsageKey, GivenNameID, NameLocaleID, BirthYearFrom, BirthYearTo, MaleWeight, FemaleWeight, UsageWeight, Confidence, SourceType, SourceReference, IsActive, Notes)
VALUES (source.UsageKey, source.GivenNameID, source.NameLocaleID, source.BirthYearFrom, source.BirthYearTo, source.MaleWeight, source.FemaleWeight, source.UsageWeight, source.Confidence, source.SourceType, source.SourceReference, source.IsActive, source.Notes);
END;
GO
CREATE OR ALTER PROCEDURE dbo.NameLocale_List
AS
BEGIN
SET NOCOUNT ON;
SELECT *
FROM dbo.NameLocales
WHERE IsActive = 1
ORDER BY SortOrder, DisplayName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.GivenName_Recognise
@NormalisedName nvarchar(120),
@LocaleCode nvarchar(16) = NULL,
@BirthYear int = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @GivenNameID int;
DECLARE @NameLocaleID int;
SELECT TOP (1) @GivenNameID = GivenNameID
FROM dbo.GivenNames
WHERE NormalisedName = @NormalisedName COLLATE Latin1_General_100_CI_AI_SC
AND IsActive = 1;
SELECT TOP (1) @NameLocaleID = NameLocaleID
FROM dbo.NameLocales
WHERE IsActive = 1
AND LocaleCode = COALESCE(@LocaleCode, N'en-US');
IF @NameLocaleID IS NULL
SELECT TOP (1) @NameLocaleID = NameLocaleID FROM dbo.NameLocales WHERE LocaleCode = N'en-US' AND IsActive = 1;
SELECT *
FROM dbo.GivenNames
WHERE GivenNameID = @GivenNameID;
IF @GivenNameID IS NOT NULL
BEGIN
SELECT TOP (1)
u.*,
l.LocaleCode,
l.DisplayName AS LocaleDisplayName,
CASE
WHEN @BirthYear IS NULL THEN N'General'
WHEN @BirthYear BETWEEN u.BirthYearFrom AND u.BirthYearTo THEN N'ExactEra'
ELSE N'NearestEra'
END AS YearMatchKind
FROM dbo.NameUsages u
INNER JOIN dbo.NameLocales l ON l.NameLocaleID = u.NameLocaleID
WHERE u.GivenNameID = @GivenNameID
AND u.NameLocaleID = @NameLocaleID
AND u.IsActive = 1
ORDER BY
CASE WHEN @BirthYear IS NOT NULL AND @BirthYear BETWEEN u.BirthYearFrom AND u.BirthYearTo THEN 0 ELSE 1 END,
CASE WHEN @BirthYear IS NULL THEN ABS(1960 - ((u.BirthYearFrom + u.BirthYearTo) / 2)) ELSE ABS(@BirthYear - ((u.BirthYearFrom + u.BirthYearTo) / 2)) END,
u.UsageWeight DESC;
EXEC dbo.GivenName_Relationships @GivenNameID = @GivenNameID;
END;
END;
GO
CREATE OR ALTER PROCEDURE dbo.GivenName_Relationships
@GivenNameID int
AS
BEGIN
SET NOCOUNT ON;
SELECT
r.NameRelationshipID,
r.RelationshipKey,
r.FromGivenNameID,
r.ToGivenNameID,
f.DisplayName AS FromDisplayName,
t.DisplayName AS ToDisplayName,
r.RelationshipType,
r.Strength,
r.IsBidirectional,
r.IsActive,
r.CurationSource,
r.Notes
FROM dbo.NameRelationships r
INNER JOIN dbo.GivenNames f ON f.GivenNameID = r.FromGivenNameID
INNER JOIN dbo.GivenNames t ON t.GivenNameID = r.ToGivenNameID
WHERE r.IsActive = 1
AND (r.FromGivenNameID = @GivenNameID OR (r.IsBidirectional = 1 AND r.ToGivenNameID = @GivenNameID))
ORDER BY r.Strength DESC, t.DisplayName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.GivenName_Search
@UserID int,
@ProjectID int,
@BookID int = NULL,
@Search nvarchar(120) = NULL,
@LocaleCode nvarchar(16) = NULL,
@BirthYear int = NULL,
@GenderPreference nvarchar(12) = NULL,
@GenderThreshold decimal(9,4) = NULL,
@NameType nvarchar(40) = NULL,
@MinimumUsage decimal(9,4) = NULL,
@UsedScope nvarchar(20) = N'Project',
@UsedOnly bit = NULL,
@Page int = 1,
@PageSize int = 25
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND UserID = @UserID AND IsActive = 1)
BEGIN
SELECT TOP (0) CAST(NULL AS int) AS GivenNameID;
RETURN;
END;
IF @Page < 1 SET @Page = 1;
IF @PageSize < 1 SET @PageSize = 25;
IF @PageSize > 100 SET @PageSize = 100;
DECLARE @NameLocaleID int = NULL;
IF @LocaleCode IS NOT NULL
BEGIN
SELECT TOP (1) @NameLocaleID = NameLocaleID FROM dbo.NameLocales WHERE LocaleCode = @LocaleCode AND IsActive = 1;
END;
;WITH UsageChoice AS
(
SELECT
u.*,
l.LocaleCode,
l.DisplayName AS LocaleDisplayName,
ROW_NUMBER() OVER (PARTITION BY u.GivenNameID ORDER BY
CASE WHEN @BirthYear IS NOT NULL AND @BirthYear BETWEEN u.BirthYearFrom AND u.BirthYearTo THEN 0 ELSE 1 END,
CASE WHEN @BirthYear IS NULL THEN ABS(1960 - ((u.BirthYearFrom + u.BirthYearTo) / 2)) ELSE ABS(@BirthYear - ((u.BirthYearFrom + u.BirthYearTo) / 2)) END,
u.UsageWeight DESC) AS rn
FROM dbo.NameUsages u
INNER JOIN dbo.NameLocales l ON l.NameLocaleID = u.NameLocaleID
WHERE u.IsActive = 1
AND (@NameLocaleID IS NULL OR u.NameLocaleID = @NameLocaleID)
),
Used AS
(
SELECT g.GivenNameID, COUNT(DISTINCT c.CharacterID) AS UsedCount,
STRING_AGG(CONVERT(nvarchar(max), c.CharacterName + N' (' + p.ProjectName + COALESCE(N' / ' + b.BookTitle, N'') + N')'), N'; ') AS UsedSummary
FROM dbo.GivenNames g
INNER JOIN
(
SELECT c.CharacterID, c.ProjectID, c.CharacterName, c.CharacterName AS MatchedName
FROM dbo.Characters c
WHERE c.IsArchived = 0
UNION ALL
SELECT c.CharacterID, c.ProjectID, c.CharacterName, ca.Alias AS MatchedName
FROM dbo.CharacterAliases ca
INNER JOIN dbo.Characters c ON c.CharacterID = ca.CharacterID
WHERE c.IsArchived = 0
) c ON UPPER(LTRIM(RTRIM(c.MatchedName))) = g.NormalisedName COLLATE Latin1_General_100_CI_AI_SC
INNER JOIN dbo.Projects p ON p.ProjectID = c.ProjectID
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID AND pua.UserID = @UserID AND pua.IsActive = 1
LEFT JOIN dbo.Books b ON b.ProjectID = c.ProjectID AND b.IsArchived = 0
WHERE (@UsedScope = N'Library' OR c.ProjectID = @ProjectID)
AND (@BookID IS NULL OR @UsedScope <> N'Book' OR b.BookID = @BookID)
GROUP BY g.GivenNameID
),
Related AS
(
SELECT x.GivenNameID,
STRING_AGG(CONVERT(nvarchar(max), x.DisplayName), N', ') AS RelatedForms
FROM
(
SELECT r.FromGivenNameID AS GivenNameID, t.DisplayName
FROM dbo.NameRelationships r
INNER JOIN dbo.GivenNames t ON t.GivenNameID = r.ToGivenNameID
WHERE r.IsActive = 1
UNION
SELECT r.ToGivenNameID AS GivenNameID, f.DisplayName
FROM dbo.NameRelationships r
INNER JOIN dbo.GivenNames f ON f.GivenNameID = r.FromGivenNameID
WHERE r.IsActive = 1 AND r.IsBidirectional = 1
) x
GROUP BY x.GivenNameID
),
Filtered AS
(
SELECT
g.GivenNameID, g.GivenNameKey, g.DisplayName, g.NormalisedName, g.NameType, g.IsWordAmbiguous,
u.MaleWeight, u.FemaleWeight, u.UsageWeight, u.Confidence, u.BirthYearFrom, u.BirthYearTo, u.LocaleCode, u.LocaleDisplayName, u.SourceType,
rel.RelatedForms,
COALESCE(used.UsedSummary, N'') AS UsedSummary,
COALESCE(used.UsedCount, 0) AS UsedCount,
COUNT(1) OVER() AS TotalRows
FROM dbo.GivenNames g
LEFT JOIN UsageChoice u ON u.GivenNameID = g.GivenNameID AND u.rn = 1
LEFT JOIN Used used ON used.GivenNameID = g.GivenNameID
LEFT JOIN Related rel ON rel.GivenNameID = g.GivenNameID
WHERE g.IsActive = 1
AND (@Search IS NULL OR g.NormalisedName LIKE UPPER(LTRIM(RTRIM(@Search))) + N'%' COLLATE Latin1_General_100_CI_AI_SC OR g.DisplayName LIKE @Search + N'%')
AND (@NameType IS NULL OR g.NameType = @NameType)
AND (@MinimumUsage IS NULL OR u.UsageWeight >= @MinimumUsage)
AND (@UsedOnly IS NULL OR (@UsedOnly = 1 AND COALESCE(used.UsedCount, 0) > 0) OR (@UsedOnly = 0 AND COALESCE(used.UsedCount, 0) = 0))
AND (
@GenderPreference IS NULL
OR @GenderThreshold IS NULL
OR (@GenderPreference = N'Female' AND u.FemaleWeight >= @GenderThreshold)
OR (@GenderPreference = N'Male' AND u.MaleWeight >= @GenderThreshold)
OR (@GenderPreference = N'Mixed' AND u.MaleWeight >= (1 - @GenderThreshold) AND u.FemaleWeight >= (1 - @GenderThreshold))
)
)
SELECT *
FROM Filtered
ORDER BY
CASE WHEN @Search IS NOT NULL AND NormalisedName = UPPER(LTRIM(RTRIM(@Search))) COLLATE Latin1_General_100_CI_AI_SC THEN 0 ELSE 1 END,
UsageWeight DESC,
DisplayName
OFFSET (@Page - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;
END;
GO
CREATE OR ALTER PROCEDURE dbo.GivenName_UsedDecorations
@UserID int,
@ProjectID int,
@BookID int = NULL,
@NormalisedName nvarchar(120),
@UsedScope nvarchar(20) = N'Project'
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND UserID = @UserID AND IsActive = 1)
RETURN;
SELECT
rows.MatchedName,
rows.MatchType,
c.CharacterID,
c.CharacterName,
p.ProjectID,
p.ProjectName,
b.BookID,
b.BookTitle,
b.Subtitle AS BookSubtitle,
CASE WHEN b.BookID IS NOT NULL THEN N'Book' ELSE N'Project' END AS Scope
FROM
(
SELECT c.CharacterID, c.ProjectID, c.CharacterName AS MatchedName, N'Character' AS MatchType
FROM dbo.Characters c
WHERE c.IsArchived = 0
UNION ALL
SELECT ca.CharacterID, c.ProjectID, ca.Alias AS MatchedName, N'Alias' AS MatchType
FROM dbo.CharacterAliases ca
INNER JOIN dbo.Characters c ON c.CharacterID = ca.CharacterID
WHERE c.IsArchived = 0
) rows
INNER JOIN dbo.Characters c ON c.CharacterID = rows.CharacterID
INNER JOIN dbo.Projects p ON p.ProjectID = c.ProjectID
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID AND pua.UserID = @UserID AND pua.IsActive = 1
LEFT JOIN dbo.SceneCharacters sc ON sc.CharacterID = c.CharacterID
LEFT JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0
LEFT JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.IsArchived = 0
LEFT JOIN dbo.Books b ON b.BookID = ch.BookID AND b.IsArchived = 0
WHERE UPPER(LTRIM(RTRIM(rows.MatchedName))) = @NormalisedName COLLATE Latin1_General_100_CI_AI_SC
AND (@UsedScope = N'Library' OR c.ProjectID = @ProjectID)
AND (@BookID IS NULL OR @UsedScope <> N'Book' OR b.BookID = @BookID)
ORDER BY p.ProjectName, b.SortOrder, c.CharacterName;
END;
GO