The Design of Software (CLOSED)

A public forum for discussing the design of software, from the user interface to the code architecture. Now closed.

The "Design of Software" discussion group has been merged with the main Joel on Software discussion group.

The archives will remain online indefinitely.

Dealing with hierarchical data

How would you deal with data like categories for a product, where you need to be able to check any entry's parent?

For example, given the following category structure:

Office Supplies (id: 1, parent id: Null)
  - Binders (id: 2, parent id: 1)
    - Three-Ring Binders (id: 3, parent id: 2)
  - Envelopes (id: 4, parent id : 1)

assuming that an individual product is linked to a subcategory (e.g. Three-Ring Binders), how would I go about getting all products whose ancestor is Office Supplies?  For example, if I had three items, one whose parent category is Binders, one whose parent category is Envelopes, and one whose parent category is Three-Ring Binders, I should be able to retrieve all three of them if I want to see all Office Supplies.

In particular this situation is brought about by the need to apply a discount to the price based on which category a product is in, but the category to apply the discount can sometimes be a top-level category (e.g. Office Supplies), and sometimes can be a sub-category several levels in (e.g. Binders|Envelopes, Three-Ring Binders).  The logic essentially must go:

- Check a product's parent category.
- If it matches something on the discount list, apply the specified discount to the product.
- If it doesn't, check that category's parent.
- If this category matches, apply the discount to the product.
- If it doesn't, check one category higher. 
- Repeat until there are no ancestor categories left.  At this point if there's no match, apply a flat discount.

This doesn't even seem like something that can be accomplished with straight SQL.
Blank for now
Tuesday, November 11, 2008
 
 
I assume that the most specific discount wins?  Save 10% on binders, 20% on 3 ring binders means 3 ring binders are 20% off.

If you use uniqueidentifiers for your ids, you can store all discounts in one table with a targetID field.  Otherwise you need to store the discount, the target type, and the target id.  (I assume there is also a qualifier like date or coupon code or something too).

So we could have a table of discounts on binders, as well as a special sale on 3 ring binders.  If you structure your parentage right, you can even make parentage recursive for an arbitrary number of layers

binders/3 ring binders/3 ring plastic binders/3 ring plastic binders with pictures of superheroes/3 ring plastic binders with pictures of superman

But I digress...

To get the discount for a given item, you would write a function that takes the target id of the item and whatever qualifiers (dates or customer id or coupon code or whatever).  Then have the function do a lookup in the appropriate place.

It would determine if there is a discount for a given item, return it, else return the item's parent discount.  If there is none, it returns the parent's parent and so on.

Of course you would make this thing recursive so you don't hardcode the number of generations to walk.

So if SELECT * FROM T_DISCOUNTS WHERE TargetID =@TargetID is mot null, you return the discount.  If it is null, you get the parent ID of the current object and call the same function from inside itself using the parentID.  If parentID is null, return zero.
Brian Begy Send private email
Tuesday, November 11, 2008
 
 
If you know the max depth of your categories tree you can write one query to return the requested "root" category and all child categories. You could create this as stored procedure.


-- first level (starting root category)
SELECT 123 AS CategoryID
UNION

-- second level
SELECT C2.CategoryID FROM Categories C2
WHERE C2.Parent = 123
UNION

-- third level
SELECT C3.CategoryID FROM Categories C3
WHERE C3.Parent IN
    (SELECT C2.CategoryID FROM Categories C2
    WHERE C2.Parent = 123)
UNION

-- four level
SELECT C4.CategoryID FROM Categories C4
WHERE C4.Parent IN
    (SELECT C3.CategoryID FROM Categories C3
    WHERE C3.Parent IN
        (SELECT C2.CategoryID FROM Categories C2
        WHERE C2.Parent = 123)

-- etc down to max depth
DJ Send private email
Tuesday, November 11, 2008
 
 
"This doesn't even seem like something that can be accomplished with straight SQL. "

Correct. Why would you even want to?
uggh
Tuesday, November 11, 2008
 
 
"Correct. Why would you even want to?"

Because I have a business requirement to apply a percentage discount to all products in our database by this categorical listing?
Blank for now
Tuesday, November 11, 2008
 
 
But is your business requirement to do it using only SQL? If so, then that is a ridiculous business requirement. SQL is used to select data, not implement complex business requirements like pricing rules.
uggh
Tuesday, November 11, 2008
 
 
By the way, as pricing rules go this one is as easy as it gets. Do yourself a favor and setup an infrastructure to support more complex pricing rules in the future. Because they WILL come, they always come. And when they do I guarantee you that you won't be able to do it using only SQL.
uggh
Tuesday, November 11, 2008
 
 
Very true.

So my best bet would be to use code and then do something like... pull a list of the products and their categories, and loop through the hierarchy, applying the discount if there's a match?
Blank for now
Tuesday, November 11, 2008
 
 
you could have a table with all the parents of an item, for each item you simply copy the parents of its parent, and add the parent, you could also have a field with the level (0 being the direct parent- you'd add 1 to the previous parents and set the actual parent to 0).
Totally Agreeing
Tuesday, November 11, 2008
 
 
Some databases can do it. Oracle and Postgres support the connect by statement.
fh Send private email
Tuesday, November 11, 2008
 
 
> how would I go about getting all products whose ancestor is Office Supplies?

There's more than one way to do it, but you can do it with one SQL select statement if you use the "nested set" model for your data, or (alternatively) if you use the SQL "Like" predicate with each node's ancestry stored as a string.

The first entry from http://www.google.ca/search?hl=en&q=nested+set&meta= is http://dev.mysql.com/tech-resources/articles/hierarchical-data.html ... there's also a book on the subject, http://www.amazon.com/Hierarchies-Smarties-Kaufmann-Management-Systems/dp/1558609202
Christopher Wells Send private email
Tuesday, November 11, 2008
 
 
OP, which database are you using?
Postgres, Oracle, IBM/Informix & MS SQL
can all do hierarchy queries, but the
syntax is vendor specific.

Oh, and I advise you to take any talk about
"SQL is not meant for business logic" as a
symptom that that programmer is too stupid
to do complex SQL.
Object Hater
Wednesday, November 12, 2008
 
 
"Oh, and I advise you to take any talk about
"SQL is not meant for business logic" as a
symptom that that programmer is too stupid
to do complex SQL. "

Wow! That's harsh. Maybe I just prefer simpler, maintainable, and more flexible solutions to trying to extend a language beyond what it was meant to do "just because". Just because you CAN write complex business logic using SQL doesn't mean you should.

I tend to believe that "object hater" has never had to maintain a customizable enteprise system consisting of millions of lines of code and dozens of different pricing methodologies with new ones being added every six months. If so, he would never advocate doing complex pricing logic in SQL.
uggh
Wednesday, November 12, 2008
 
 
No, I had to maintain a customizable enteprise system
consisting of millions of lines of code and hundreds of
different pricing methods that changed whenever the user
damn well felt like it, even multiple times in one day.
We had to do it in COM objects, which were an inflexible
maintenance nightmare. I'd choose SQL over that any day.
Object Hater
Wednesday, November 12, 2008
 
 
If you change your entire data storage/search/retrieval system to make this one thing easier, all the other things that SQL makes easy are going to become harder.

But if it was easy, we'ld all be out of a job and the MBAs would write the business logic themselves.  :)

Wednesday, November 12, 2008
 
 
Not in SQL? We do it all in stored procedures. If you have some advanced and responsible SQL developers, I have yet to see anything you cannot do with _data_ in SQL.

Structure your data in an appropriate manner, and leave the logic juggling to SQL, keep it in a stored procedure.

If I have a SKU, and I need to obtain pricing details, it should be as simple as passing the product id (or perhaps the order details as xml) to the sproc and get a dataset that tell you what each product cost, selling price is, what discounts you have, order level discounts... special discounts because you bought an xyz and a efg together?

In my experience these rules change so often within organizations, I would never want to put them in compiled code. This week we charge freight on orders less than 500, next week we have new kinds of promotions, the week after freight not in.

Is this so bad?
ajax
Thursday, November 13, 2008
 
 
"We had to do it in COM objects, which were an inflexible
maintenance nightmare. I'd choose SQL over that any day. "

But I'd choose C# over both COM and SQL any day. But hey, maybe that's just me.

My motto is generally to use the simpler solution which helps reduce bugs and maintenance costs. So given the choice of a super complex SQL implementation vs. a significantly simpler C# implementation I'm going to go with C#. Does that mean that I don't get a lot of experience writing ultra-complex SQL? You bet. And I consider that to be a good thing. I have yet to find a problem that really required the use of ultra-complex SQL. And most of the ultra-complex SQL I've run up against in my career was very buggy and performed very poorly which ultimately lead to its demise at the hands of the refactoring gods.  ;)

I'm not saying that being able to craft complex SQL isn't beneficial. But it is highly overrated when your goal should usually be to craft the simplest SQL possible.

We can agree to disagree if you'd like. But please refrain from calling me names simply because I don't agree with you.
uggh
Thursday, November 13, 2008
 
 
"Is this so bad? "

It depends on your circumstances. If you have a single data center with a single database then changing SQL is often easier than changing code. But in our case we have databases all over the country. I won't go into the details because you can search for my name and find where I've done this rant before.

Obviously, every situation is different. What works for someone sitting in a data center where they can physically touch the database server does not work for someone whose only access to thousands of databases is over a dial-up connection running something like ConnectRemote.

I personally can't imagine having to implement our retailer's complex pricing algorithms in SQL. They are so convoluted that they can often take thousands of lines of Java code and loop through the transaction multiple times (think "best pricing algorithms"). For those types of things I find code to be better than SQL. YMMV.
dood mcdoogle
Thursday, November 13, 2008
 
 
Uggh, I'll call you whatever the hell names I like as long
as you insist on asserting what my job is. My goal isn't
to craft the simplest SQL possible, it is to produce the
best solution possible.

And I don't know when Ted Codd passed his crown to you,
so maybe you could stop arrogating to yourself the right
to dictate what SQL is for. Instead, you could say
something you can actually back up, like "*I personally*
find it easier to put business rules outside the database".

Oh, and if you find the same complex logic easier in
crappy, unexpressive, low-level C# than in declarative,
expressive, high-level SQL, I'd want to see you do a
Computer Science GRE subject test before I'd hire you.
Object Hater
Thursday, November 13, 2008
 
 
"Obviously, every situation is different. What works for someone sitting in a data center where they can physically touch the database server does not work for someone whose only access to thousands of databases is over a dial-up connection running something like ConnectRemote."

Of course you are correct, and I've been in both situations. The right tool for the job... as they say

My current situation is as you say -  a maintained data center with SQL gurus having easy access.

Systems distributed to customers... we once used a SQL patching system that would update versions of stored procs at a scheduled time, each location had an update bot that would replace dll's, update sprocs, update db version, client version, verify client=server version, all that fancy stuff, and notified the patching server that everyone was updated properly.

We loved it (and feared it).. (and sometimes hated it).. but by golly it worked!

However, it wasn't easy to build such a reliable patching system, and of course it bit us in the hindquarters a few times... but it did allow us to relegate the ever-changing business rules of a distributed retail system to stored procedures, where we could be very agile and responsive to business changes.
ajax
Thursday, November 13, 2008
 
 
All I can say is WOW! You really have anger issues. It appears that you don't just "hate objects". You appear to just hate people in general as this is not the only time that you have clearly broken down out here and reverted to hostile responses. You might want to seek help with that.

And you would never get to hire me anyway because you wouldn't last two seconds in my interview of your people skills.

It's just as well. Oil and water don't mix at all. I've worked too many times with people like you who can't take even minor criticism of their beloved SQL. Some people will argue to death that SQL is the best way to implement everything. But I've worked on too many systems where people tried to get too clever using SQL for things it wasn't designed for and the result was always the same: a buggy, poorly performing, hard to maintain system that was ultimately replaced prematurely with a system that had the right balance of code and SQL to get the job done. That's all I'm advocating here. The right tool for the right job.
uggh
Thursday, November 13, 2008
 
 
By the way, this is exactly what I'm talking about...

http://discuss.joelonsoftware.com/default.asp?joel.3.697800.21
uggh
Thursday, November 13, 2008
 
 
Yes, I have an anger issue: Bullshitters make me angry.
Bullshit is when you say something you are not entitled
to say. This is science, not a matter of opinion.
I've suffered from too many projects misdirected
and wrecked by bullshitters who wanted to push their
own opinion over the correct design.

Take that example thread you gave. That was spaghettiware,
not correct use of SQL. If you had pushed the same
spaghettiware logic out into C# it would have been
infinitely worse because the source code size would
probably have ballooned. So the problem was that the
logic was wrong and needed to be refactored, not the
syntax used to express it.

So it is a LIE to use that as an example against business
logic in SQL and you are not entitled to say that. The same
way you are not entitled to say 1+1=3. This is science, not
a matter of opinion.

The same thing with the crap you spout about what SQL
was designed for. RDBMS querying was NOT meant only for
atomic CRUD operations. It is and has always been a
version of Prolog cut down in order to perform well. This
is documented in the record. You are simply not entitled
to say "SQL wasn't designed for business logic". This is
science, not a matter of opinion.

Go ahead and criticize SQL if you can actually back up
what you say. I myself have, on these boards, criticized how
the configuration and source can be difficult to control when
you put the business logic in SQL. It's the fact that you
bullshitted that made me angry, just like other bullshitters
before you made me angry.

It is a heuristic simply supported by all of my experience
that the people who bullshit about SQL not being meant for
anything but minimal storage operations are the ones who
build spaghettiware SQL systems, because they can't query
properly to save their lives. I'll take your word for it
that you have baseline competence.
Object Hater
Thursday, November 13, 2008
 
 
"Bullshit is when you say something you are not entitled
to say. "

Am I not entitled to say whatever I want to say? Just as you are entitled to ignore me? I'm not aware of any governing body that could have appointed you the power to judge decide what I am and am not entitled to say.

Wow! I just can't express how amazed I am at your anger over this issue. You don't agree with me. Fine, move on. But continuously attacking me and expressing such anger over what amounts to no more than an OPINION is ridiculous. I can certainly imagine that many people don't agree with me on my stance about when to use SQL and when not to. But I would be shocked if anyone else on this forum disagrees with me as to the severity of your anger problem related to these post (and other posts I've seen you make out here in the past).

I'm not going to fuel your rage anymore. If you only truly listen to one thing that I say today I hope that it is this: get help with your anger problem. Life is way too short to be angry at a complete stranger on the Internet just because he prefers a different solution to what you prefer. Seriously! I mean it...
uggh
Thursday, November 13, 2008
 
 
Get help...
uggh
Thursday, November 13, 2008
 
 
Now!
uggh
Thursday, November 13, 2008
 
 
Stop reading...
uggh
Thursday, November 13, 2008
 
 
Go get help!
uggh
Thursday, November 13, 2008
 
 
If you can store your data with full paths instead of just parent id you can very easily query for discounts for a given product. Works fine as long as your paths don't change very often since they are a pain to update.

Products table

Name                          Path
====                          ====
Office Supplies              /
Binders                      /OfficeSupplies/
Three-Ring Binders            /OfficeSupplies/Binders/
Envelopes                    /OfficeSupplies/
Three-Ring Plastic Binders    /OfficeSupplies/Binders/Three-Ring Binders/
Window Envelopes              /OfficeSupplies/Envelopes/


Discounts table

Percentage  Path
==========  ====
10          /OfficeSupplies/Binders/
15          /OfficeSupplies/Binders/Three-Ring Binders/
20          /OfficeSupplies/Envelopes/Window Envelopes/

The following SQL return below result

SELECT
    Name, Percentage
FROM
    Products, Discounts
WHERE Products.Path + Products.Name  + '/' LIKE Discounts.Path + '%' AND
    LEN(Discounts.Path) = (SELECT MAX(LEN(PATH)) FROM Discounts DiscountsInner
                          WHERE Products.Path + Products.Name  + '/' LIKE DiscountsInner.Path + '%')


Name                        Percentage 
====                        ============
Binders                    10         
Three-Ring Binders          15         
Three-Ring Plastic Binders  15         
Window Envelopes            20
pjsson Send private email
Thursday, November 13, 2008
 
 
Uggh, you are projecting clinically
pathological rage onto me just
because I don't tolerate your bullshit.

"I like vanilla" is an opinion.
"SQL is not designed for logic" is
bullshit. See, you didn't design SQL,
so you don't get to say what it was
for. The people who did design it told
us what it was for already.

So, that statement is factually untrue.
For you to say it is mistaken verging
on fraud-through-negligence. That
is why you are not entitled to say it.
You should have learned this in logic
class in high school.

It has nothing to do with whether I am
calm or angry (I am perfectly calm as
I write this) and nothing to do with
your opinion (you are perfectly entitled
to say "I like vanilla" as much as you
like").

See, at this point you get to retract your
statement, or you get to say something like
"Err sorry, I didn't mean to talk about
what SQL was designed for, I meant to talk
about how I prefer to use it".

You don't get to persist in saying something
factually wrong, just like you can't insist
that 1+1=3 by saying "that's just my opinion".

Also you don't get to project emotions onto
me in order to distract from the wrongness of
your claim: "Wow, that dude said that 1+1=3
is bullshit, the problem must obviously be
with him, he needs anger management".

You want to be ignored? Fine, I am putting
you into the "do not respond" bucket to keep
Lemon OBrien company.

You recommend I get help? Fine, I recommend
a career in sales to you. You'll probably
earn more than a programmer anyway.
Object Hater
Thursday, November 13, 2008
 
 
So if I simply say that "It is my OPINION that SQL wasn't designed for business logic" then your whole argument goes out the window? Didn't I already say that several times or at least imply it to any rationally thinking human being? I think so.

You have no more right to say what was in the original SQL designer's heads than I do. And for the record, things like stored procedures and conditional constructs were NOT part of the original SQL designer's implementation. So it is hard to argue that SQL was originally designed for complex business logic when the original implementation provided no means for even implementing such logic. SQL has certainly changed a lot since the early days (which I was around for) and now CAN be used for business logic. Again, my point is about COMPLEX business logic and not business logic in general. Regardless, you might want to consider this last paragraph carefully before claiming to know what was in the minds of the original SQL designers.

I fail to see how you have any say in what I am and am not entitled to say. I learned that in Social Studies class. And what I learned in logic class is that nothing you've said makes any sense at all.

Just let it go. There is really no more for you to say on the issue other than to admit your anger and mistakes. And if you want to call what I've said complete bullshit then please feel free to do so. I am not entitled to tell you what you are and are not allowed to say about my opinions.

I'm not sure why you singled me out as my thoughts on complex business logic in the database are shared by many others out here. Many people express these same thoughts over and over again on this forum. Maybe I'm getting special consideration from you because you've disagreed with me elsewhere on this forum. Perhaps you are a pot smoker as well? ;) That would certainly answer some nagging questions in my mind. (sorry, I couldn't resist)

Anyway, I can't imagine needing to respond to anything else you say. So feel free to blast away without fear of reprisal.

To the OP, I'm sorry this got so ugly. I really didn't mean to hijack your thread. Although I like to think I was acting in self-defense most of the time I have to admit that I am partly to blame for this mess. And for that I am truly sorry. I hope you got the info you needed.
uggh
Friday, November 14, 2008
 
 
object hater - what the hell's your problem? You are acting like an ass.

People can say whatever they want to say. Stop making yourself look stupid.
WTF?
Friday, November 14, 2008
 
 
oh yeah, ugh I don't agree with you on business logic and sql. But at least I'm not going to tell you you can't say whatever the hell you want to.
WTF?
Friday, November 14, 2008
 
 
Okay everyone thanks for the replies.  Lucky for me it turns out that our supplier provides a special "categorized" field that's not a category for "grouping" purposes.  I can make use of this field and change the business rules that apply the discounts to use those fields instead (they're arbitrary), that way I don't need to traverse hierarchies at all but just look up what grouping a product is contained under.
Blank for now
Friday, November 14, 2008
 
 
Awesome! Glad to hear it.
uggh
Friday, November 14, 2008
 
 
I'm sorry, WTF, people can't say what they want because then
you get bullshit.

Uggh could opine that "sql ISN'T  FIT for business logic". S/he's
not entitled to say  "sql WASN'T DESIGNED for business logic"
because when s/he does, that's bullshit.

And, *not* meaning to be mean, but I have to stand by my first
post. (Which was not aimed specifically at Uggh - as I see it,
s/he's been the one who was escalating.) There are tell tale
signs.

If all I have to go on is what Uggh posted here, s/he is too dumb
to be a developer. A decent person probably, but dumb. I'd peg
Uggh's IQ at about 125 tops. If s/he were working for me, I'd
yell at Uggh and then stick Uggh in the systems/requirements
analysis team.

And try and save Uggh's ass from getting laid off.
Object Hater
Friday, November 14, 2008
 
 
My IQ is actually 143 (and dropping). I've been tested multiple times. But IQ means nothing at all. It is a meaningless number.

And you still didn't address the fact that the original designers of SQL didn't include support for stored procedures, conditional contructs, or any other language features that would allow you to write complex business logic. I don't see how you can argue this point any further given that SQL was clearly designed (by folks at IBM) as a data query language only.

Wikipedia says:

"SQL (Structured Query Language) (pronounced /ɛskjuːˈɛl /) is a database computer language designed for the retrieval and management of data in relational database management systems (RDBMS), database schema creation and modification, and database object access control management.[2][3]"

Notice how it doesn't mention business logic at all? It's great that you can use SQL today to implement complex business logic. But that was not the original intention of the designers. And the fact that there are some databases which support "SQL" but not features that allow you to write business logic (sp's, etc.) should be another telltale sign.

You can ignore me all you want and call me names. You can claim that I am an idiot and need to be protected from being layed off. But it should be clear to everyone else on this forum that you have lost this argument and that I am entitled to say whatever I want to. And you are not entitled to tell me otherwise.

And, I am male...
uggh
Saturday, November 15, 2008
 
 
OK I'll bite, for the last damned time.

It doesn't matter a damn what wikipedia says.
The IBM database (and concurrently Ingres) were
the attempted implementations of Codd's
"A Relational Model of Data for Large Shared Data Banks"
and SQL was an attempt at implementing the query
language specified in the paper* **.

The paper intends the full power of the mathematical
theory of relations to be applied to the data. Read it
carefully. Though it does not emphasize these, it
specifically allows for calculations, recursive queries
and complex logic to the level of an expert system to be
available in the query language.

They did not get around to implementing all features in
version 1, but it is still possible to do complex business
logic in SQL without case clauses and stored procedures.
I do it all the time, if you are half as good at SQL as you
claim you should know how to too.

And yes, henceforth I will ignore you.

*http://www.cs.uiuc.edu/class/fa07/cs511/papers/codd.pdf
**http://scholar.google.co.uk/scholar?num=100&hl=en&lr=&safe=off&client=firefox-a&q=codd+data+banks&btnG=Search
Object Hater
Saturday, November 15, 2008
 
 
"but it is still possible to do complex business
logic in SQL without case clauses and stored procedures.
I do it all the time, if you are half as good at SQL as you
claim you should know how to too."

Just because you could have created "business logic" (*see my point below about this) in V1 of SQL using ridiculous hacks doesn't mean that the original designers meant it to be that way. I can dig a hole with a hammer but the original designers of that hammer didn't intend for that to be it's primary function. SQL was meant to be a query language first and foremost. It STILL is a query language first and foremost.

*And I think your definition of "business logic" needs some adjusting. What you could do with V1 of SQL was not business logic by anyone else's definition. So nice try but no dice.

And not once in this whole ridiculous discussion did I say that I was an expert on SQL. Quite the contrary. I said that I'm not good at creating complex SQL because I don't do it often. And I consider that to be a good thing. You clearly don't but I'm not going to tell you what you can and can't think.

Honestly, I only started down the whole "SQL didn't originally support stored procedures" angle because you started the pedantic argument about how I couldn't possibly know what the original designers intended. I really don't care what they intended. I just wanted to show that you clearly didn't know what they intended either.

You are just so sure that I have no right to speak my mind which is absolutely ridiculous. You come up with some new classification of opinion called "bullshit" and outlaw it. Well I can tell you that what you speak is utter bullshit so you are no longer entitled to speak it! So there!

If I want to say that 1+1=3 then I can. I am completely entitled to say that over and over again until I am dead. And you are entitled to tell the world how wrong I am and that 1+1=2. But you are NOT entitled to tell the world that I have to stop saying it because it is "bullshit".

I truly am done with this. I won't be back. It is clear that you are one of those guys who simply can't admit when he has made a mistake. I've had to work with your kind many times over the years and it is pure hell. I actually feel sorry for you. Unfortunately, that trait is very prevalent in our industry and on this forum. It's one of the main reasons why "geeks" are looked down upon by those outside of our industry.

Try looking in the mirror once in a while and recognizing that none of us are perfect. I'm certainly not perfect and I make mistakes all of the time. I say stupid things constantly. However, the difference between people like me and you is that I try to admit when I've made a mistake. I actually make a concious effort to recognize this trait in myself and correct it when possible. And I apologize to those I've wronged.

Again, I feel very, very sorry for you... good luck!
uggh
Sunday, November 16, 2008
 
 

This topic is archived. No further replies will be accepted.

Other recent topics Other recent topics
 
Powered by FogBugz