April 22, 2015 at 2:55 am
I have a SQL database with a number of tables that are loaded from an old flat-file system. the process for loading one of these is now unable to cope with the volume of data so, as a short-term fix, I need to replace the table with a view composed of data from other tables. That way, applications can use the view instead of the table. My replacement data consists of an Addressee table and an AddressBlock table. The AddressBlock is a set of address lines uniquely identified by an AddressCode. The Addressee links a client to one or many addresses (eg. home address, work address, etc.). Addresses may be shared by multiple clients. Additionally, the Addressee table contains Title lines that are combined with the address lines to form the final output.
I'm running SQL Server 2008 R2.
1) Create a database to play in.
IF EXISTS #SELECT name FROM sys.databases WHERE name = N'DemoDB'#
drop DATABASE [DemoDB];
go
CREATE DATABASE [DemoDB];
go
2) Create the tables.
Use DemoDB;
go
IF OBJECT_ID('dbo.Addressee','U') IS NOT NULL
DROP TABLE dbo.Addressee;
IF OBJECT_ID('dbo.AddressBlock','U') IS NOT NULL
DROP TABLE dbo.AddressBlock;
create table dbo.Addressee(
[AddresseeID] [bigint] identity(1,1) not null,
[AddressCode] [char](10) not null,
[Client] [char](5) not null,
[UsageNum] [tinyint] not null,
[Title_1] [varchar](40) not null,
[Title_2] [varchar](40) not null,
[Title_3] [varchar](40) not null,
constraint [PK_Addressee] primary key clustered
(
[AddresseeID] asc
)with (pad_index = off, statistics_norecompute = off, ignore_dup_key = off, allow_row_locks = on, allow_page_locks = on) on [PRIMARY]
) on [PRIMARY];
create table dbo.AddressBlock(
[AddressCode] [char](10) not null,
[PostCode] [char](8) not null,
[AddressLine1] [varchar](40) not null,
[AddressLine2] [varchar](40) not null,
[AddressLine3] [varchar](40) not null,
[AddressLine4] [varchar](40) not null,
[AddressLine5] [varchar](40) not null,
[AddressLine6] [varchar](40) not null,
[AddressLine7] [varchar](40) not null,
[AddressLine8] [varchar](40) not null,
constraint [PK_AddressBlock] primary key clustered
(
[AddressCode] asc
)with (pad_index = off, statistics_norecompute = off, ignore_dup_key = off, allow_row_locks = on, allow_page_locks = on, fillfactor = 90) on [PRIMARY]
) on [PRIMARY];
go
3) Populate with data.
insert dbo.Addressee
(AddressCode,
Client,
UsageNum,
Title_1,
Title_2,
Title_3)
select'ZYADDR1', 'ZY001', 14, 'Marjorie Dibbs', 'ZY Holdings Ltd.', '' union all
select'ZYADDR1', 'ZY001', 1, 'ZY Holdings Ltd.', '', '' union all
select'ZYADDR1', 'ZY001', 15, 'Marjorie Dibbs', 'CEO', 'ZY Holdings Ltd.' union all
select'ZYADDR1', 'ZY002', 1, 'ZY Partners LLP', '', '' union all
select'ZYADDR1', 'ZY002', 14, 'Stuart Dibbs', 'ZY Partners LLP', '' union all
select'DIBBADDR', 'ZY002', 13, 'Mr. S. Dibbs', '', '' union all
select'DIBBADDR', 'ZY001', 13, 'Mrs. Marjorie Dibbs', '', '';
insert dbo.AddressBlock
(AddressCode,
PostCode,
AddressLine1,
AddressLine2,
AddressLine3,
AddressLine4,
AddressLine5,
AddressLine6,
AddressLine7,
AddressLine8)
select'ZYADDR1', 'BB01 1AB', '4th Floor', 'Office Block', 'Sloane Square', 'London', '', '', '', '' union all
select'ZYADDR2', 'BB01 1AB', '2nd Floor', 'Office Block', 'Sloane Square', 'London', '', '', '', '' union all
select'DIBBADDR', 'AA01 1DI', 'Rose Cottage', '1 High St.', 'Lower Pagwell', 'Gloucester', 'Gloucestershire', '', '', '';
4) My solution uses a function based on the DelimitedSplit8k and a view to provide the formatted output (note: the actual function comments include all the credits from DelimitedSplit8k, but I left them out here for the sake of brevity).
create function [dbo].[ufn_SplitAddress]
/* Based on the famed DelimitedSplit8k
Rev 10 - 21 Apr 2015 - Chris Wooding
Stripped down a bit to handle up to 500 characters and up to 12 elements for use in
splitting addresses. Also return the address lines as columns in one row instead of
multiple rows.
**********************************************************************************************************************/
-- Define I/O parameters
(@pString varchar(500), @pDelimiter char(1))
returns table with schemabinding as
return
-- "Inline" CTE Driven "Tally Table" produces values from 0 up to 100
with E1(N) as (
select 1 union all select 1 union all select 1 union all
select 1 union all select 1 union all select 1 union all
select 1 union all select 1 union all select 1 union all select 1
), --10E+1 or 10 rows
E2(N) as (select 1 from E1 a, E1 b), --10E+2 or 100 rows
cteTally(N) as (-- This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
select top (isnull(datalength(@pString),0)) row_number() over (order by (select null)) from E2
),
cteStart(N1) as (-- This returns N+1 (starting position of each "element" just once for each delimiter)
select 1 union all
select t.N+1 from cteTally t where substring(@pString,t.N,1) = @pDelimiter
),
cteLen(N1,L1) as(-- Return start and length (for use in substring)
select s.N1,
isnull(nullif(charindex(@pDelimiter,@pString,s.N1),0)-s.N1,500)
-- The isnull/NULLIF combo handles the length for the final element when no delimiter is found.
from cteStart s
)
-- Do the actual split and pivot rows into columns.
select [1], [2], [3], [4], [5], [6],
[7], [8], [9], [10], [11], [12]
from (select substring(@pString, l.N1, l.L1) as AddressLine, row_number() over(order by l.N1) as ItemNumber
from cteLen l) as AddressLines
pivot (max(AddressLine)
for ItemNumber in ([1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12])) as AddressRows
;
go
create view [dbo].[FormattedAddress] as
with CompressedAddress as (
selectdistinct
a.AddressCode, ab.PostCode,
case
when a.Title_1 > '' then
replace(replace(replace(
a.Title_1 + '|' +
a.Title_2 + '|' +
ab.AddressLine1 + '|' +
ab.AddressLine2 + '|' +
ab.AddressLine3 + '|' +
ab.AddressLine4 + '|' +
ab.AddressLine5 + '|' +
ab.AddressLine6 + '|' +
ab.AddressLine7 + '|' +
ab.AddressLine8 + '|' +
-- Ensure the 2 postcode parts are separated by only a single space
replace(replace(replace(rtrim(ab.PostCode), ' ', '{}'), '}{', ''), '{}', ' ')
-- Remove consecutive delimiters
, '|', '{}'), '}{', ''), '{}', '|')
else
replace(replace(replace(
ab.AddressLine1 + '|' +
ab.AddressLine2 + '|' +
ab.AddressLine3 + '|' +
ab.AddressLine4 + '|' +
ab.AddressLine5 + '|' +
ab.AddressLine6 + '|' +
ab.AddressLine7 + '|' +
ab.AddressLine8 + '|' +
replace(replace(replace(rtrim(ab.PostCode), ' ', '{}'), '}{', ''), '{}', ' ')
, '|', '{}'), '}{', ''), '{}', '|')
end as AddressLines
from dbo.Addressee a
inner join dbo.AddressBlock ab on a.AddressCode = ab.AddressCode),
SplitAddress as (
select ca.AddressCode, ca.PostCode, sa.*
from CompressedAddress ca
cross apply dbo.ufn_SplitAddress(ca.AddressLines, '|') sa)
selectAddressCode,
row_number() over (partition by AddressCode order by AddressCode) as AddressInstance,
PostCode,
isnull(s.[1], '') as AddressLine1,
isnull(s.[2], '') as AddressLine2,
isnull(s.[3], '') as AddressLine3,
isnull(s.[4], '') as AddressLine4,
isnull(s.[5], '') as AddressLine5,
isnull(s.[6], '') as AddressLine6,
isnull(s.[7], '') as AddressLine7,
isnull(s.[8], '') as AddressLine8
from SplitAddress s
go
5) The expected output is shown in the attached spreadsheet. My solution gives this, but is too slow over the 180k AddressBlock and 530k Address rows in our database. I'm hoping someone here can see a way to speed it up.
Thanks
April 22, 2015 at 9:33 am
Hi Chris
here is an alternative method....works on your sample data....probably need a bit of tidy up
WITH cte as (
SELECT
ROW_NUMBER() OVER (order by (select null) ) - ROW_NUMBER() OVER (partition by AddresseeID order by A.AddressCode) AS rn2
,A.AddressCode
,AddLine
,addvalue
FROM Addressee AS A INNER JOIN
AddressBlock AS AB ON A.AddressCode = AB.AddressCode
CROSS APPLY
( values
('add1', title_1),
('add2', title_2),
('add3', AddressLine1),
('add4', AddressLine2),
('add5', AddressLine3),
('add6', AddressLine4),
('add7', AddressLine5),
('add8', AddressLine6),
('add9', Postcode)
) c (AddLine,addvalue)
)
, cte2 as (
SELECT
rn2
, ROW_NUMBER() OVER (PARTITION BY rn2 ORDER BY (SELECT NULL) ) AS rn3
, AddressCode
, AddLine
, addvalue
FROM cte
WHERE addvalue > ''
)
SELECT
AddressCode
, max (case when rn3 = 1 then addvalue else null end) add1
, max (case when rn3 = 2 then addvalue else null end) add2
, max (case when rn3 = 3 then addvalue else null end) add3
, max (case when rn3 = 4 then addvalue else null end) add4
, max (case when rn3 = 5 then addvalue else null end) add5
, max (case when rn3 = 6 then addvalue else null end) add6
, max (case when rn3 = 7 then addvalue else null end) add7
FROM cte2
GROUP BY AddressCode, rn2
ORDER BY AddressCode
, max (case when rn3 = 1 then addvalue else null end)
, max (case when rn3 = 2 then addvalue else null end)
________________________________________________________________
you can lead a user to data....but you cannot make them think
and remember....every day is a school day
April 22, 2015 at 11:13 am
This is what I came up with:
SELECT
ab.AddressCode,
AddressInstance = ROW_NUMBER() OVER (Partition by ab.AddressCode ORDER BY (SELECT (NULL))),
ab.PostCode,
AddressLine1 = title_1,
AddressLine2 = CASE WHEN Title_2 = '' THEN ab.AddressLine1 ELSE Title_2 END,
AddressLine3 = CASE WHEN Title_2 = '' THEN ab.AddressLine2 ELSE ab.AddressLine1 END,
AddressLine4 = CASE WHEN Title_2 = '' THEN ab.AddressLine3 ELSE ab.AddressLine2 END,
AddressLine5 = CASE WHEN Title_2 = '' THEN ab.AddressLine4 ELSE ab.AddressLine3 END,
AddressLine6 = CASE WHEN Title_2 = '' THEN ISNULL(NULLIF(ab.AddressLine5,''),ab.PostCode) ELSE ab.AddressLine4 END,
AddressLine7 =
CASE
WHEN
(Title_2 <> '' AND ab.AddressLine5 = '') OR
(Title_2 = '' AND ab.AddressLine5 <> '')
THEN ab.PostCode
ELSE ''
END,
AddressLine8 = '' -- don't understand a scenario where this would not be blank
FROM dbo.AddressBlock ab
JOIN dbo.Addressee a ON a.AddressCode = ab.AddressCode
You might need to test my solution with more data but this works with the data provided.
Furthermore, you can increase the performance of what I posted by pretty dramatically by adding this index:
CREATE NONCLUSTERED INDEX nc_addressee_addresscode ON dbo.Addressee (AddressCode)
INCLUDE (Title_1, Title_2);
-- Itzik Ben-Gan 2001
April 22, 2015 at 11:38 am
Alan.B (4/22/2015)
This is what I came up with:
SELECT
ab.AddressCode,
AddressInstance = ROW_NUMBER() OVER (Partition by ab.AddressCode ORDER BY (SELECT (NULL))),
ab.PostCode,
AddressLine1 = title_1,
AddressLine2 = CASE WHEN Title_2 = '' THEN ab.AddressLine1 ELSE Title_2 END,
AddressLine3 = CASE WHEN Title_2 = '' THEN ab.AddressLine2 ELSE ab.AddressLine1 END,
AddressLine4 = CASE WHEN Title_2 = '' THEN ab.AddressLine3 ELSE ab.AddressLine2 END,
AddressLine5 = CASE WHEN Title_2 = '' THEN ab.AddressLine4 ELSE ab.AddressLine3 END,
AddressLine6 = CASE WHEN Title_2 = '' THEN ISNULL(NULLIF(ab.AddressLine5,''),ab.PostCode) ELSE ab.AddressLine4 END,
AddressLine7 =
CASE
WHEN
(Title_2 <> '' AND ab.AddressLine5 = '') OR
(Title_2 = '' AND ab.AddressLine5 <> '')
THEN ab.PostCode
ELSE ''
END,
AddressLine8 = '' -- don't understand a scenario where this would not be blank
FROM dbo.AddressBlock ab
JOIN dbo.Addressee a ON a.AddressCode = ab.AddressCode
You might need to test my solution with more data but this works with the data provided.
Furthermore, you can increase the performance of what I posted by pretty dramatically by adding this index:
CREATE NONCLUSTERED INDEX nc_addressee_addresscode ON dbo.Addressee (AddressCode)
INCLUDE (Title_1, Title_2);
Alan...this is very similar to what I was originally going to post.....but I dont think it deals correctly if an address line in blank/NULL
code to test
IF OBJECT_ID('dbo.Addressee','U') IS NOT NULL
DROP TABLE dbo.Addressee;
IF OBJECT_ID('dbo.AddressBlock','U') IS NOT NULL
DROP TABLE dbo.AddressBlock;
create table dbo.Addressee(
[AddresseeID] [bigint] identity(1,1) not null,
[AddressCode] [char](10) not null,
[Client] [char](5) not null,
[UsageNum] [tinyint] not null,
[Title_1] [varchar](40) not null,
[Title_2] [varchar](40) not null,
[Title_3] [varchar](40) not null,
constraint [PK_Addressee] primary key clustered
(
[AddresseeID] asc
)with (pad_index = off, statistics_norecompute = off, ignore_dup_key = off, allow_row_locks = on, allow_page_locks = on) on [PRIMARY]
) on [PRIMARY];
create table dbo.AddressBlock(
[AddressCode] [char](10) not null,
[PostCode] [char](8) not null,
[AddressLine1] [varchar](40) not null,
[AddressLine2] [varchar](40) not null,
[AddressLine3] [varchar](40) not null,
[AddressLine4] [varchar](40) not null,
[AddressLine5] [varchar](40) not null,
[AddressLine6] [varchar](40) not null,
[AddressLine7] [varchar](40) not null,
[AddressLine8] [varchar](40) not null,
constraint [PK_AddressBlock] primary key clustered
(
[AddressCode] asc
)with (pad_index = off, statistics_norecompute = off, ignore_dup_key = off, allow_row_locks = on, allow_page_locks = on, fillfactor = 90) on [PRIMARY]
) on [PRIMARY];
go
insert dbo.Addressee
(AddressCode,
Client,
UsageNum,
Title_1,
Title_2,
Title_3)
select'ZYADDR1', 'ZY001', 14, 'Marjorie Dibbs', 'ZY Holdings Ltd.', '' union all
select'ZYADDR1', 'ZY001', 1, 'ZY Holdings Ltd.', '', '' union all
select'ZYADDR1', 'ZY001', 15, 'Marjorie Dibbs', 'CEO', 'ZY Holdings Ltd.' union all
select'ZYADDR1', 'ZY002', 1, 'ZY Partners LLP', '', '' union all
select'ZYADDR1', 'ZY002', 14, 'Stuart Dibbs', 'ZY Partners LLP', '' union all
select'DIBBADDR', 'ZY002', 13, 'Mr. S. Dibbs', '', '' union all
select'DIBBADDR', 'ZY001', 13, 'Mrs. Marjorie Dibbs', '', '';
insert dbo.AddressBlock
(AddressCode,
PostCode,
AddressLine1,
AddressLine2,
AddressLine3,
AddressLine4,
AddressLine5,
AddressLine6,
AddressLine7,
AddressLine8)
select'ZYADDR1', 'BB01 1AB', '4th Floor', '', 'Sloane Square', 'London', '', '', '', '' union all
select'ZYADDR2', 'BB01 1AB', '2nd Floor', 'Office Block', '', 'London', '', '', '', '' union all
select'DIBBADDR', 'AA01 1DI', 'Rose Cottage', '1 High St.', 'Lower Pagwell', 'Gloucester', 'Gloucestershire', '', '', '';
________________________________________________________________
you can lead a user to data....but you cannot make them think
and remember....every day is a school day
April 22, 2015 at 12:04 pm
J Livingston SQL (4/22/2015)
Alan.B (4/22/2015)
This is what I came up with:
SELECT
ab.AddressCode,
AddressInstance = ROW_NUMBER() OVER (Partition by ab.AddressCode ORDER BY (SELECT (NULL))),
ab.PostCode,
AddressLine1 = title_1,
AddressLine2 = CASE WHEN Title_2 = '' THEN ab.AddressLine1 ELSE Title_2 END,
AddressLine3 = CASE WHEN Title_2 = '' THEN ab.AddressLine2 ELSE ab.AddressLine1 END,
AddressLine4 = CASE WHEN Title_2 = '' THEN ab.AddressLine3 ELSE ab.AddressLine2 END,
AddressLine5 = CASE WHEN Title_2 = '' THEN ab.AddressLine4 ELSE ab.AddressLine3 END,
AddressLine6 = CASE WHEN Title_2 = '' THEN ISNULL(NULLIF(ab.AddressLine5,''),ab.PostCode) ELSE ab.AddressLine4 END,
AddressLine7 =
CASE
WHEN
(Title_2 <> '' AND ab.AddressLine5 = '') OR
(Title_2 = '' AND ab.AddressLine5 <> '')
THEN ab.PostCode
ELSE ''
END,
AddressLine8 = '' -- don't understand a scenario where this would not be blank
FROM dbo.AddressBlock ab
JOIN dbo.Addressee a ON a.AddressCode = ab.AddressCode
You might need to test my solution with more data but this works with the data provided.
Furthermore, you can increase the performance of what I posted by pretty dramatically by adding this index:
CREATE NONCLUSTERED INDEX nc_addressee_addresscode ON dbo.Addressee (AddressCode)
INCLUDE (Title_1, Title_2);
Alan...this is very similar to what I was originally going to post.....but I dont think it deals correctly if an address line in blank/NULL
code to test
IF OBJECT_ID('dbo.Addressee','U') IS NOT NULL
DROP TABLE dbo.Addressee;
IF OBJECT_ID('dbo.AddressBlock','U') IS NOT NULL
DROP TABLE dbo.AddressBlock;
create table dbo.Addressee(
[AddresseeID] [bigint] identity(1,1) not null,
[AddressCode] [char](10) not null,
[Client] [char](5) not null,
[UsageNum] [tinyint] not null,
[Title_1] [varchar](40) not null,
[Title_2] [varchar](40) not null,
[Title_3] [varchar](40) not null,
constraint [PK_Addressee] primary key clustered
(
[AddresseeID] asc
)with (pad_index = off, statistics_norecompute = off, ignore_dup_key = off, allow_row_locks = on, allow_page_locks = on) on [PRIMARY]
) on [PRIMARY];
create table dbo.AddressBlock(
[AddressCode] [char](10) not null,
[PostCode] [char](8) not null,
[AddressLine1] [varchar](40) not null,
[AddressLine2] [varchar](40) not null,
[AddressLine3] [varchar](40) not null,
[AddressLine4] [varchar](40) not null,
[AddressLine5] [varchar](40) not null,
[AddressLine6] [varchar](40) not null,
[AddressLine7] [varchar](40) not null,
[AddressLine8] [varchar](40) not null,
constraint [PK_AddressBlock] primary key clustered
(
[AddressCode] asc
)with (pad_index = off, statistics_norecompute = off, ignore_dup_key = off, allow_row_locks = on, allow_page_locks = on, fillfactor = 90) on [PRIMARY]
) on [PRIMARY];
go
insert dbo.Addressee
(AddressCode,
Client,
UsageNum,
Title_1,
Title_2,
Title_3)
select'ZYADDR1', 'ZY001', 14, 'Marjorie Dibbs', 'ZY Holdings Ltd.', '' union all
select'ZYADDR1', 'ZY001', 1, 'ZY Holdings Ltd.', '', '' union all
select'ZYADDR1', 'ZY001', 15, 'Marjorie Dibbs', 'CEO', 'ZY Holdings Ltd.' union all
select'ZYADDR1', 'ZY002', 1, 'ZY Partners LLP', '', '' union all
select'ZYADDR1', 'ZY002', 14, 'Stuart Dibbs', 'ZY Partners LLP', '' union all
select'DIBBADDR', 'ZY002', 13, 'Mr. S. Dibbs', '', '' union all
select'DIBBADDR', 'ZY001', 13, 'Mrs. Marjorie Dibbs', '', '';
insert dbo.AddressBlock
(AddressCode,
PostCode,
AddressLine1,
AddressLine2,
AddressLine3,
AddressLine4,
AddressLine5,
AddressLine6,
AddressLine7,
AddressLine8)
select'ZYADDR1', 'BB01 1AB', '4th Floor', '', 'Sloane Square', 'London', '', '', '', '' union all
select'ZYADDR2', 'BB01 1AB', '2nd Floor', 'Office Block', '', 'London', '', '', '', '' union all
select'DIBBADDR', 'AA01 1DI', 'Rose Cottage', '1 High St.', 'Lower Pagwell', 'Gloucester', 'Gloucestershire', '', '', '';
I agree about the Blank/NULL situation. I should have been more clear that what I posted was not a final solution. I do think it could be tweaked to handle blanks/nulls I just did not have enough time.
My main point (that I did not articulate) was that concatenating then splitting the address in the way that the OP did it was certainly not necessary. Your solution demonstrates this as well.
-- Itzik Ben-Gan 2001
April 23, 2015 at 1:44 am
Thanks for your responses to this.
J Livingstone, I got an error message trying to create a view with your code; "The ORDER BY clause is invalid in views, inline functions, etc. etc.". I need this to be a view because the intention is to remove the existing table and give the view the name of the table so that calling applications don't see a change.
Alan, I am getting quite a few duplicate lines with your solution (but it is much faster than mine).
I had trouble creating the original post and there are some clarifications missing. The AddressBlock table has up to 8 lines, but in practice the last two are always blank. The 3 Title lines may be present or blank, but will always be populated in order (the same applies to the Address lines with the additional condition that at least 1 line will always be present). In the final output, I need the 8 lines to be distinct. The AddressInstance column simply indicates how many times an AddressCode is used and the order in which it is incremented is not important (in fact, rather than incrementing, it could be replaced by a simple count because the only time an application uses it, it selects the MAX value for an AddressCode).
Chris
April 23, 2015 at 2:21 am
Update: I tried the J Livingstone solution as a straight SELECT rather than a view definition. It takes about the same time as my solution and has the same duplicates problem as Alan's.
Further data clarification; a client can link to an address multiple times with different values of UsageNum. Often there will be different Title lines for each, but sometimes this is not the case. I think this is the cause of the duplications in the posted solutions. If you use the replacement Addressee data below, it illustrates this situation (the second row is the new one). The final results should still match those in my original spreadsheet.
insert dbo.Addressee
(AddressCode,
Client,
UsageNum,
Title_1,
Title_2,
Title_3)
select'ZYADDR1', 'ZY001', 14, 'Marjorie Dibbs', 'ZY Holdings Ltd.', '' union all
select'ZYADDR1', 'ZY001', 3, 'Marjorie Dibbs', 'ZY Holdings Ltd.', '' union all
select'ZYADDR1', 'ZY001', 1, 'ZY Holdings Ltd.', '', '' union all
select'ZYADDR1', 'ZY001', 15, 'Marjorie Dibbs', 'CEO', 'ZY Holdings Ltd.' union all
select'ZYADDR1', 'ZY002', 1, 'ZY Partners LLP', '', '' union all
select'ZYADDR1', 'ZY002', 14, 'Stuart Dibbs', 'ZY Partners LLP', '' union all
select'DIBBADDR', 'ZY002', 13, 'Mr. S. Dibbs', '', '' union all
select'DIBBADDR', 'ZY001', 13, 'Mrs. Marjorie Dibbs', '', '';
Viewing 7 posts - 1 through 6 (of 6 total)
You must be logged in to reply to this topic. Login to reply