Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 General SQL Server Forums
 New to SQL Server Programming
 Customized table sort order logic

Author  Topic 

weitzhandler
Yak Posting Veteran

64 Posts

Posted - 2009-04-15 : 19:25:01
Function:
CREATE FUNCTION GetId(@TableName sysname)
RETURNS int
AS
BEGIN
DECLARE
@Id int,
@Sql nvarchar(1024) = 'SELECT IdOUT = TOP(1) ' + @TableName + 'Id FROM ' + @TableName + ' ORDER BY ' + @TableName + 'Id DESC',
@ParmDefinition nvarchar(256) = 'IdOUT int OUTPUT'

EXEC sp_executesql
@Sql,
@ParmDefinition,
@IdOUT = @Id OUTPUT

RETURN @Id
END


Usage:
SELECT dbo.Getid('Contact')


Result:
Msg 557, Level 16, State 2, Line 1
Only functions and some extended stored procedures can be executed from within a function.



Now I know that executing procedures from within funcs is not support (which really sucks), I am just looking for a way so I can have this function in other script or whatever.
Note, I want to use this function as a default-value value so I want it in a function an not as a SP.

Any discussion would be really appreciated.
Thanks!


Shimmy

TG
Master Smack Fu Yak Hacker

6065 Posts

Posted - 2009-04-15 : 22:50:02
Can't say it any clearer than the error message you got. Forget about trying to EXEC anything from within a function.

>>Now I know that executing procedures from within funcs is not support
you know sp_executesql is a stored procedure, right?

What are you planning on doing with this ID once you have it? If you are using it to increment the value and then add a new row, that method won't hold up during concurrent requests. 2 or more people get the same ID and then all but one gets a PK violation. IDENTITY columns were developed to solve that exact problem.

Be One with the Optimizer
TG
Go to Top of Page

weitzhandler
Yak Posting Veteran

64 Posts

Posted - 2009-04-15 : 23:38:45
Well, why don't tell you my exact problem:

Here is the code (you can create a test database for testing):
USE Test

CREATE TABLE Contact(
ContactId int not null IDENTITY(1,1),
Name varchar(64) not null
)

CREATE TABLE Phone(
PhoneId int not null IDENTITY(1,1),
Phone varchar(32) not null,
ContactId int not null,
SortOrder int not null
)
GO

ALTER TABLE Contact ADD CONSTRAINT
PK_Contact PRIMARY KEY CLUSTERED (ContactId) ON [PRIMARY]

ALTER TABLE Phone ADD CONSTRAINT
PK_Phone PRIMARY KEY CLUSTERED (PhoneId) ON [PRIMARY]

ALTER TABLE dbo.Phone ADD CONSTRAINT
IX_Phone UNIQUE NONCLUSTERED
(
ContactId,
SortOrder
)
GO

ALTER TABLE dbo.Phone ADD CONSTRAINT
FK_Phone_Contact FOREIGN KEY
(ContactId) REFERENCES dbo.Contact(ContactId)
ON UPDATE NO ACTION
ON DELETE CASCADE

GO


Now let me explain briefly, as you saw we have a parent contact table that has many child phone tables.
I want the user to have control on the order of the phones according to each user.
On the other hand, I don't want things to get messy which means:

  • Strictly consistent groups - under each contact parent should be a consistent zero based chaine of sort order values.

  • If there are 5 phones for this contact, there shouldn't be SortOrder = 7 (or SortOrder = 5 as it's zero based and max allowed is 4).

  • I have already set up the unique key for the groups so there won't be duplicates.

  • There are few more, I don't remember or I am too lazy now...



Anyway, I think you got the picture.
My problem begins now since I don't have only a Contact->Phone pair but also Building->Address (I need 1 building to have many addresses in a custom order), or I have a Job table and a Contractor table and I want to have a third party header table JobContractorHeader(JobId int, ContractorId int, SortOrder int) to connect between them while having the control on each jobs' contractors' ordered within this job) and so on and so on.
Let's say that I have in my application more than 20 tables that provide this SortOrder column.

You understand that this is rediculus to start writing insert update and delete sprocs for each of them, I thought about moving things to get a bit more dynamic.

First I developed a Procedure that handles changing of existing SortOrder values (this PROC handles the whole logic meaning pushing back and forward all the corresponding rows' SortOrder values).
Second - which we're standing here - I want to have a function that I will be able to set up the DEFAULT value of the SortOrder col, for instance, if the above function would of been avilable I could use:
[dbo].[GetSortOrder](
'Phone', --passes table name
'ContactId', --passes the group column name
[ContactId] --passes the group parent
)

Now you can chose whether to help me with this particular function from above, or to help me with the entire issue (I donno, maybe there is some built in API that provides it all...), or not help at all, anyway if you reached till here I already owe you, lolz.

Any further discussion will be really appreciated.

Shimmy
Go to Top of Page

Transact Charlie
Master Smack Fu Yak Hacker

3451 Posts

Posted - 2009-04-16 : 05:22:21
Functions cannot change the state of the database in any way. Also they need to be completely deterministic (at least in 2000 / 2005). I.E given the same inputs (and the same underlying data) they will always produce the same output. To make sure function's can't modify data the data engine won't let you call stored procs from them.

rewrite the function a a stored proc that either SELECT's the ID as a result set or includes it as a OUTPUT parameter depending on how you want to use it.


Charlie
===============================================================
Msg 3903, Level 16, State 1, Line 1736
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION
Go to Top of Page

weitzhandler
Yak Posting Veteran

64 Posts

Posted - 2009-04-16 : 05:26:41
It suck but OK.
Thanks for your help, I did it with a SPROC...

(Pity, I only wanted to select data, not to change, so I could use the function as the default value of the column rather than creating triggers)

Shimmy
Go to Top of Page

weitzhandler
Yak Posting Veteran

64 Posts

Posted - 2009-04-16 : 23:17:08
quote:
Originally posted by weitzhandler

Well, why don't tell you my exact problem:
.............
.............
Shimmy




Here you go, A a dynamic procedure that handles the entire logic:

USE [Bss]
GO
/****** Object: StoredProcedure [dbo].[sp_sortorder] Script Date: 04/17/2009 06:07:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- ================================================
-- Description: Handles the SortOrder column logic.
-- For tables that provide it.
-- @TableName sysname, The table name you want to apply the logic on.
-- @IdColumn varchar(32) = null, Specify the ID column name if other than @TableName + 'ID'
-- @RowId, The ID of the row you want to apply the logic on.
-- @SortOrderColumn varchar(32) = 'SortOrder', The name of the column that exposes the SortOrder.
-- @SortOrderValue int = -1, The new SortOrder value - ifargument is -1, will change to max value in the SortOrder.
-- @ParentIdColumn varchar(32) = null, If you want to limit the sorting to a sub-group, pass the parent-table's name.
-- ================================================
CREATE PROCEDURE [dbo].[sp_sortorder]
(
@TableName sysname,
@IdColumn varchar(32) = null,
@RowId int,
@SortOrderColumn varchar(32) = 'SortOrder',
@SortOrderValue int = -1,
@ParentIdColumn varchar(32) = null
)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;

DECLARE
@CurrentSortOrder int, -- Current SortOrder value for the specified row.
@ParentId int, -- The parent ID to limit the group with.
@Group varchar(256) = '', -- The sql syntax that will limit the group.
@Direction bit

DECLARE @SortOrderData TABLE (RowId int not null, SortOrder int not null)

SET @IdColumn = ISNULL(@IdColumn, @TableName + 'ID')

IF NOT ISNULL(@ParentIdColumn, '') = ''
BEGIN
DECLARE
@Sql nvarchar(1024) = N'SELECT @CurrentSortOrderOUT = ' + @SortOrderColumn + ', @ParentIdOUT = ' + @ParentIdColumn + ' FROM ' + @TableName + ' WHERE ' + @IdColumn + ' = ' + ISNULL(CAST(@RowId AS varchar(10)), '0'),
@ParmDefinition nvarchar(256) = '@CurrentSortOrderOUT int OUTPUT, @ParentIdOUT int OUTPUT'
EXECUTE sp_executesql
@Sql,
@ParmDefinition,
@CurrentSortOrderOUT = @CurrentSortOrder OUTPUT,
@ParentIdOUT = @ParentId OUTPUT
END

IF @SortOrderValue >= 0 AND @SortOrderValue = @CurrentSortOrder PRINT '!!!!!!! R E T U R N E D !!!!!!!!' --RETURN

SET @Group =
CASE WHEN ISNULL(@ParentIdColumn, '') = ''
THEN ''
ELSE ' WHERE ' + @ParentIdColumn + ' = ' + ISNULL(CAST(@ParentId AS varchar(10)), '0')
END

--Select the entire group including our row.
INSERT INTO @SortOrderData(RowId, SortOrder)
EXEC (N'SELECT ' + @IdColumn + ', ' + @SortOrderColumn + ' FROM ' + @TableName + @Group + ' ORDER BY ' + @SortOrderColumn)

--Set current row's data:
SELECT @CurrentSortOrder = SortOrder FROM @SortOrderData WHERE RowId = @RowId

DECLARE @Max int = (SELECT TOP(1) SortOrder FROM @SortOrderData ORDER BY SortOrder DESC)

IF @SortOrderValue < 0
SET @SortOrderValue = @Max
ELSE IF @SortOrderValue > @Max
BEGIN
SET @SortOrderValue = @Max
END
IF @SortOrderValue = @CurrentSortOrder PRINT '!!!!!!! R E T U R N E D 2!!!!!!!!' --RETURN


SET @Direction =
CASE
WHEN @CurrentSortOrder < @SortOrderValue THEN 1 --1 means that the current is smaller and needs to go up (i.e. 5 to 7).
ELSE 0 --Means that it wants to move from a high SortOrder value to low (i.e. 7 to 5)
END

-----------------------------------
print 'DIRECTION:' print @direction
print 'Current sort order:' print @currentsortorder
PRINT 'SORT ORDER VALUE:' PRINT @SORTORDERVALUE
-----------------------------------

--Delete from the table all the rows we don't need for the sorting including our row.
IF @Direction = 1 DELETE FROM @SortOrderData WHERE SortOrder <= @CurrentSortOrder OR SortOrder > @SortOrderValue
ELSE DELETE FROM @SortOrderData WHERE SortOrder >= @CurrentSortOrder OR SortOrder < @SortOrderValue

BEGIN TRY
BEGIN TRANSACTION
--Temporarily change this row's SortOrder to -1 to avoid conflicts.
IF @Group <> '' SET @Group = REPLACE(@Group, 'WHERE', 'AND')
EXEC('UPDATE ' + @TableName + ' SET ' + @SortOrderColumn + ' = - 1 WHERE ' + @IdColumn + ' = ' + @RowId + @Group)

DECLARE
@Id int,
@SortOrder int,
@Dir CHAR(3) = CASE WHEN @Direction = 1 THEN ' - ' ELSE ' + ' END

print 'FETCH============================================='
DECLARE SortOrder_Cursor CURSOR FOR
SELECT * FROM @SortOrderData
ORDER BY CASE WHEN @Direction = 1 THEN SortOrder ELSE -SortOrder END

OPEN SortOrder_Cursor
FETCH NEXT FROM SortOrder_Cursor INTO @Id, @SortOrder
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT 'current fetch: ' print @id print @SortOrder
EXEC('UPDATE ' + @TableName + ' SET ' + @SortOrderColumn + ' = ' + @SortOrder + @Dir + '1 WHERE ' + @IdColumn + ' = ' + @Id)
FETCH NEXT FROM SortOrder_Cursor INTO @Id, @SortOrder
END
CLOSE SortOrder_Cursor
DEALLOCATE SortOrder_Cursor

EXEC('UPDATE ' + @TableName + ' SET ' + @SortOrderColumn + ' = ' + @SortOrderValue + ' WHERE ' + @IdColumn + ' = ' + @RowId + @Group)
COMMIT
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
PRINT 'TRAN CANCELLED: ' + error_message()
END CATCH
END


Shimmy
Go to Top of Page

weitzhandler
Yak Posting Veteran

64 Posts

Posted - 2009-04-16 : 23:19:59
And here is a SPROC (Not a function :( so I can't use in DEFAULT, only in triggers) that retrieves the next available SortOrder value:

USE [Bss]
GO

/****** Object: StoredProcedure [dbo].[sp_getsortorder] Script Date: 04/17/2009 06:18:06 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- Author: <Author,,Name>
-- Create date: <Create Date,,>
-- Description: <Description,,>
-- =============================================
CREATE PROCEDURE [dbo].[sp_getsortorder]
-- Add the parameters for the function here
@TableName sysname,
@ParentIdColumn varchar(32) = null,
@ParentId int = null,
@SortOrderColumn varchar(32) = 'SortOrder'
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;

-- Insert statements for procedure here
DECLARE
@SortOrder int,
@Group nvarchar(1024) = ''


IF NOT ISNULL(@ParentIdColumn, '') = ''
IF ISNULL(@ParentId, '') = '' RAISERROR('If parameter @ParentIdColumn is specified, parameter @ParentId must be specified too.', 15, 1)
ELSE SET @Group = ' WHERE ' + @ParentIdColumn + ' = ' + ISNULL(CAST(@ParentId AS nvarchar(16)), '0')

DECLARE @Sql nvarchar(1024) = N'SELECT TOP(1) @SortOrderOUT = ' + @SortOrderColumn + ' FROM ' + @TableName + @Group + ' ORDER BY ' + @SortOrderColumn + ' DESC',
@ParmDefinition nvarchar(256) = '@SortOrderOUT int OUTPUT'

print @Sql

EXEC sp_executesql
@Sql,
@ParmDefinition,
@SortOrderOUT = @SortOrder OUTPUT

SELECT @SortOrder

END

GO




Shimmy
Go to Top of Page

Transact Charlie
Master Smack Fu Yak Hacker

3451 Posts

Posted - 2009-04-17 : 09:01:48
you could make a view that contains UNIONED queries and then reference that from a function without having to resort to dynamic sql.

-- That's assume that the table names themselves aren't dynamic? -- you are not doing something crazy like making base tables on the fly?

Say you have 3 tables (Foo, Bar, Woo)

You could have a view with 2 columns (Table_Name, Next_available) That holds queries that produce data like below
Table_Name | Next Available
---------------------------
foo | 45
bar | 12
woo | 1


And then have a function that references the view with a table_name parameter) -- the function would work as it doesn't involve any dynamic sql or calls to other stored procedures.

I'm still not convinced why you would want to do this but I think it could be implemented that way.


Charlie
===============================================================
Msg 3903, Level 16, State 1, Line 1736
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION
Go to Top of Page

weitzhandler
Yak Posting Veteran

64 Posts

Posted - 2009-04-18 : 18:05:52
could you please post a brief sample on how to create such a view.

Shimmy
Go to Top of Page

Transact Charlie
Master Smack Fu Yak Hacker

3451 Posts

Posted - 2009-04-20 : 04:43:34
Here:

IF OBJECT_ID('_sortOrders') IS NOT NULL DROP VIEW _sortOrders
IF OBJECT_ID('_tableA') IS NOT NULL DROP TABLE _tableA
IF OBJECT_ID('_tableB') IS NOT NULL DROP TABLE _tableB
GO

CREATE TABLE _tableA (
[cola] INT
, [colb] INT
)

CREATE TABLE _tableB (
[cola] INT
)
GO

INSERT INTO _tableA ([cola], [colb])
SELECT 1,2
UNION SELECT 323,0
UNION SELECT 0,123

INSERT INTO _tableB(cola)
SELECT 0
UNION SELECT 1
GO

CREATE VIEW _sortOrders AS
SELECT TOP (1) 'tableA' AS [table], 'cola' AS [column], 'DES' AS [Direction], [cola] AS [value] FROM _tableA ORDER BY [colA] DESC
UNION ALL SELECT TOP (1) 'tableA', 'cola', 'ASC', [cola] FROM _tableA ORDER BY [colA] ASC
UNION ALL SELECT TOP (1) 'tableA', 'colb', 'DES', [colb] FROM _tableA ORDER BY [colB] DESC
UNION ALL SELECT TOP (1) 'tableB', 'cola', 'ASC', [cola] FROM _tableB ORDER BY [cola] ASC
GO

SELECT * FROM dbo._sortOrders

OF course it will be slow as it will query all the tables rather than target the one you are after.

I really think you should rethink your position on a general procedure / function to do this -- it's almost always a bad idea as the database engine just isn't a good tool for this.


Charlie
===============================================================
Msg 3903, Level 16, State 1, Line 1736
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION
Go to Top of Page

SwePeso
Patron Saint of Lost Yaks

30421 Posts

Posted - 2009-04-20 : 05:39:34
And you still can't use the ID in a INSERT case, since the procedure is only invoked once, and you problably will have concurrency issues.

Is there a special reason you can't use IDENTITY columns?



E 12°55'05.63"
N 56°04'39.26"
Go to Top of Page

weitzhandler
Yak Posting Veteran

64 Posts

Posted - 2009-04-20 : 05:57:17
man, it seems you really didn't understand me.

first: since when is an identity col subject to change!?
second, i said i wanted it to be dynamic so i don't have to write milion views for all my table.

the only little job is to create triggers which will call that func.
and what we have to do here is really easy now, since we only have to provide the sproc params (table name, parent id, etc.)

Shimmy
Go to Top of Page
   

- Advertisement -