Friday, 28 November 2025

Tech skillsets: Go deep or go wide? (Part 2/2)

There are plenty of reasons, though, to choose to generalize rather than specialize. And by that, I mean good reasons, not things like a lack of focus or a limited attention span.

The case for generalizing over specializing

It's dangerous to put all your eggs in one basket. An extreme example is if programmers specialized in VBScript to the exclusion of all else. The probability of those programmers having a job as of 2023, are approaching zero. I love VBScript, but it is what it is. Professionally, I cut my teeth on ASP and VBScript back in the 2000s. But what would have happened had I stubbornly refused to branch out? The thought makes me shudder.

Speaking of branching out, what makes it relatively easy for me to pick up new programming languages and frameworks is that I've been doing it repeatedly over the years. Instead of focusing on one specific thing to get good at, I identified certain things one new area had with the one I already knew, and worked from there. Got my fingers in as many related pies as possible. And as I learned more new stuff, my frame of reference only got stronger. All the stuff I learned, fed into each other. It's unclear just how good I would be at picking up new stuff if I had specialized instead.

Fingers in many pies.

The other advantage being a generalist gave me, was that I got plenty of attention from cheapskate financially strapped employers who saw the wisdom of getting me to do many different things at once, because I was decent at all those things. And often, for small startups, that's just what they need. The end result was that even in poor economic times, I rarely had trouble finding employment. I did not have to apply only for front-end jobs, or back-end jobs. I could apply to all of them.

Back in the day, I had been working for a law firm and maintaining their corporate website as one of my many duties. When I eventually left for greener pastures, the guy they hired to replace me was a more accomplished network infrastructure guy. More specialized, I would say. I went on to ply my trade as a web developer, but occasionally I would check back on my former company's corporate website to see what was shaking. Imagine my surprise one day when I went to the ABOUT US page where the hotshot attorneys were presented, and found a whole bunch of distorted profile photos! And that was after waiting a good couple minutes for all these images to load.

Apparently my replacement not only did not understand optimizing images for the internet, he also did not know about resizing images to fit the HTML img tag placeholders. He had simply taken the photographs he had been presented with, and used them as-is.

Was that his fault? No, not at all. My then-Manager had specifically wanted a network specialist, as opposed to me, a web developer who could follow simple network instructions. And his wish had been granted. But specialization comes at a cost. Things like image optimization for the web, just weren't covered in the guy's extensive networking knowledge. In the job, not only had I needed to help with network and infrastructure, I'd had to develop web applications and maintain websites, and my skillset had grown accordingly. These were the things the company had learned to take for granted where my position was concerned.

Ultimately...

Let me just say it again - the industry needs both specialists and generalists. Without generalists, specialists would have trouble plugging all the other technical gaps that their very specific skillsets do not cover. Hiring specialists for every area just isn't practical. On the other hand, without specialists, it would be devilishly hard for just generalists to raise the quality of the product to its optimal level.

Most of all, it would be really difficult to be a specialist in a world without generalists. And vice versa. Not only are both needed, the two need each other.

Specially for you,
T___T

Tuesday, 25 November 2025

Tech skillsets: Go deep or go wide? (Part 1/2)

"Jack of all trades, master of none" is a term I've heard all too often when fellow techies describe my stint as a web developer. Back then, I was a generalist dipping my sticky fingers into every new and shiny tech I encountered. In a sense, I'm still that web developer. Just with more money.

Go deep or go wide?

I want to explore the pros and cons of generalizing as opposed to specializing, especially in the context of today's tech landscape. The industry needs both specialists and generalists. That's a simple fact that shouldn't need some nobody tech blogger pointing out, but sometimes people get overly emotional about defending their stance. Want to specialize? Cool, do that. Want to generalize? Also cool, go crazy. But we need to be cognizant of the tradeoffs.

The case for specializing over generalizing

Before every tech and his dog started calling themselves "full-stack developers", specialists seemed to earn a whole lot of money. That was back when I was reading job ads asking for "deep expertise" and extensive work experience in more narrow scopes such as back-end programming and database administration. Some areas, such as COBOL programming, pay a lot for the simple reason that COBOL is still largely in use but COBOL programmers appear to be a dying breed. I've touched a lot of programming languages, but just enough to qualify as a hobbyist in each one. I have a decent frame for comparison between them - a Python lecturer of mine was actually quite entertained at my comparisons of the language to PHP and Ruby - but that often isn't very useful in the professional sense.

Specialists earn big.

The companies whose budgets are large enough to afford to pay for specialists, also tend to use these specialists for projects that are grand in scale. Projects that actually require deep expertise. Projects that add incalculable value to one's CV.

Deep knowledge is also useful for judging the extent of one's resolve and commitment. After all, it takes plenty of both to spend the effort. I personally know someone who specializes in HTML and CSS. It's a very narrow niche and perhaps not very profitable, but undeniably impressive.

Generalists - quite unfairly, I might add - seem to have developed a reputation for being easily distracted. Lacking the necessary discipline and focus required to specialize. I would actually say it takes an extraordinary amount of discipline to avoid going too deeply down one rabbit hoole and shuting out everything else, but that's not the general sentiment. Therefore, in some circles, generalists are seen as those who were unable to specialize as opposed to specialists being unable to generalize.

The conventional advice when it comes to tech specialization, is to specialize in one area while simultaneously being decent in a few related areas. For instance, if you're a Data Analytics guru, you may want to specialize in statistical analysis and mathematics, but at the same time get reasonably good with Data Visualization tools and learn a bit of Python. If you're a primarily a Front-end Developer, learning some simple database concepts would be a useful addition to HTML, CSS and JavaScript. Maybe pick up a few frameworks such as ReactJS or VueJS, just to round things out.

I won't argue against that advice, but these days it feels like nothing is ever enough.

Next

We explore the opposite case!

Wednesday, 19 November 2025

Web Tutorial: Color Troll Meme

Time for a little fun. There's a meme I encountered years back, where it claims that certain illnesses affect the eyes, and there's a test for it. If you can see what you're reading, you probably don't have that condition. So you'll be tested for serious stuff like "tuberculosis", "stress" or what-have-you, and then there will be one you can't quite make out. When you finally do, it says something embarrassing like "erectile dysfunction".

Something like this!

So yeah, let's do something like that today. Totally randomize the text and colors. We'll use VueJS for this because... why the hell not, eh?

We're going to have some boilerplate HTML to start with. We have a div with the id ctmApp that will be Vue's placeholder. There's also a script remote link to VueJS.
<!DOCTYPE html>
<html>
  <head>
    <title>Color Troll Meme</title>

    <style>

    </style>
  </head>

  <body>
    <div id="ctmApp">

    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.min.js"></script>

    <script>
  
    </script>
  </body>
</html>


Inside the script tag, We declare app as a call to Vue(). In it, we pass an object. In the classic VueJS pattern, the object will have el, data, methods and createdel will be set to ctmApp.
<script>
var app = new Vue
(
  {
    el: "#ctmApp",
    data:
    {

    },
    methods:
    {

    },
    created: function()
    {
   
    }
  }
);
</script>


In data, we want two arrays - ctms, inUse and the object wordTypes.
data:
{
  ctms:
  [
                          
  ],
  inUse:
  [

  ],
  wordTypes:
  {  

  }

},


ctms is an array of objects. You can have as many as you want, though I'd go with 9. Each object has name, color and textColor properties.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }    
                        
  ],
  inUse:
  [

  ],
  wordTypes:
  {  

  }
},


inUse is a placeholder array which defaults to empty. We won't need to worry about it yet. wordTypes is an objects that has area, condition, condition_prefix and condition_suffix arrays as properties. These arrays contain strings.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }                            
  ],
  inUse:
  [

  ],
  wordTypes:
  {  
    area:
    [

    ],
    condition:
    [

    ],
    condition_prefix:
    [

    ],
    condition_suffix:
    [

    ]

  }
},


I've filled these in. area is supposed to be embarrassing areas of the human body to develop medical conditions in. condition is an array of medical conditions that range from mild to nasty. condition_prefix and condition_suffix are descriptors that are supposed to go with the words in area.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }                            
  ],
  inUse:
  [

  ],
  wordTypes:
  {  
    area:
    [
      "testicle",
      "anus",
      "vagina",
      "nipple",
      "groin",
      "penis",
      "labia",
      "cleavage",
      "rectum"

    ],
    condition:
    [
      "dehydration",
      "constipation",
      "arthritis",
      "bronchitis",
      "myopia",
      "baldness",
      "pinkeye",
      "narcolepsy",
      "epilepsy",
      "insomnia",
      "dementia",
      "psychosis",
      "indigestion",
      "heartburn"

    ],
    condition_prefix:
    [
      "detached",
      "bruised",
      "inverted",
      "ingrown",
      "misaligned",
      "ruptured"

    ],
    condition_suffix:
    [
      "lacerations",
      "tumor",
      "ulcers",
      "blisters",
      "blockage",
      "fracture",
      "ulcers",
      "inflammation",
      "necrosis",
      "discharge",
      "dysfunction",
      "fungus"

    ]
  }
},


For the methods object, we will put in five methods - fillInfo(), getColor(), getCondition(), getOne() and getRandomNo(). In the created method, we will run the fillInfo() method.
methods:
{
  fillInfo: function()
  {
                          
  },
  getColor: function()
  {

  },
  getCondition: function()
  {

  },
  getOne: function()
  {

  },
  getRandomNo:function()
  {

  }

},
created: function()
{
  this.fillInfo();
}


In the HTML, add this in the ctmApp div. We use v-for to dictate that we want a div for each element in the ctms array, styled using the CSS class ctm.
<div id="ctmApp">
  <div class="ctm" v-for="ctm in ctms">

  </div>

</div>


In the CSS, add the CSS class for ctm. It's basically a deep grey circle.
<style>
  .ctm
  {
    width: 300px;
    height: 300px;
    float: left; 
    margin-left: 10%;
    margin-top: 10%;
    border-radius: 50%;
    background-color: rgb(10, 10, 10);
  }

</style>


You'll see 9 deep grey circles. Because I put 9 elements inside the ctms array. Depending on your screen size, you may need to scroll.


Let's work on the methods now. This is getRandomNo(). Give it a parameter, max. It should return a random number from 0 to max minus 1.
getRandomNo:function(max)
{
  return Math.floor(Math.random() * max);
}


For getOne(), we have the parameter, arr, which is an array. We return one of the elements of arr.
getOne: function(arr)
{
  return arr[];
},


Which one? Well, we let getRandomNo() decide.
getOne: function(arr)
{
  return arr[this.getRandomNo()];
},


But of course, we need to pass in the size of arr so that the random number generated is not out of bounds for arr.
getOne: function(arr)
{
  return arr[this.getRandomNo(arr.length)];
},


For getColor(), there is the parameter maxRGB. maxRGB is used to restrict how bright each color component is. In here, we declare r, g and b, and each one is set by using getRandomNo() to grab any number from 0 to maxRGB minus 1.
getColor: function(maxRGB)
{
  var r = this.getRandomNo(maxRGB);
  var b = this.getRandomNo(maxRGB);
  var g = this.getRandomNo(maxRGB);

},


Then we return an array containing these three values.
getColor: function(maxRGB)
{
  var r = this.getRandomNo(maxRGB);
  var b = this.getRandomNo(maxRGB);
  var g = this.getRandomNo(maxRGB);
  return [r, g, b];
},


And for the last small method, we have getCondition(). For this, we have the parameters area and prefix. Here, we assume that area has already been taken from the area array, and prefix is a Boolean that is true by default.
getCondition: function(area, prefix = true)
{

},


We declare cond as an empty string, then use an If block on prefix.
getCondition: function(area, prefix = true)
{
  var cond = "";

  if (prefix)
  {

  }
  else
  {

  }

},


If prefix is true, we set cond to a random element in condition_prefix using getOne(), and return a string of cond and area. If not, we set cond to a random element in condition_suffix, and return a string of area and cond.
getCondition: function(area, prefix = true)
{
  var cond = "";

  if (prefix)
  {
    cond = this.getOne(this.wordTypes.condition_prefix);
    return cond + " " + area;

  }
  else
  {
    cond = this.getOne(this.wordTypes.condition_suffix);
    return area + " " + cond;

  }
},


Now, time to work on fillInfo()! It will use all of the methods we've worked on up to now, either directly or otherwise. We begin by iterating through the ctms array.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {

    }
  );  
                          
},


We declare isLast as a Boolean. If the current value of index is exactly the length of ctms minus 1, that means it's the last element. Then we have an If block based on isLast.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {
      var isLast = (index === this.ctms.length - 1);

      if (isLast)
      {

      }
      else
      {

      }

    }
  );                            
},


We'll leave the first case along for now. For all other ctm objects in the ctms array, we set the name property to a random string from the condition array.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {
      var isLast = (index === this.ctms.length - 1);

      if (isLast)
      {

      }
      else
      {
        ctm.name = this.getOne(this.wordTypes.condition);
      }
    }
  );                            
},


Now, here in the HTML, we have a h1 tag with the value of name.
<div class="ctm" v-for="ctm in ctms">
  <h1>{{ ctm.name }}</h1>
</div>


In the HTML, let's just set the h1 tag to have a certain size, center it, and so on. Most importantly, we set the color to a light grey so we can see the contrast.
<style>
  .ctm
  {
    width: 300px;
    height: 300px;
    float: left;
    margin-left: 10%;
    margin-top: 10%;
    border-radius: 50%;
    background-color: rgb(10, 10, 10);
  }

  .ctm h1
  {
    margin-top: 40%;
    font-size: 30px;
    text-align: center;
    color: rgb(100, 100, 100);
  }

</style>


Look at this! Conditions are populated, but "pinkeye" is repeated.


To fix this, check if the value of name is in the inUse array. And keep reassigning the name property until you get a value that isn't in the inUse array.
if (isLast)
{

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }

}


Once that's done, push the value into the inUse array, so that this value can be one of the values compared against further down.
if (isLast)
{

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


Now you'll see no repeats! Bear in mind that this only works as long as you have less elements in the ctms array (OK, not counting the final element) than there are elements in the condition array. Otherwise you're going to get an infinite loop.


Now to handle the condition for the last element in ctms. Declare prefix as a Boolean, and use getRandomNo() with a argument of 2, to get a random number between 0 and 1. Then declare area, and use getOne() to obtain a random value from the area array, as its value.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


Then set the name property by using getCondition() and passing in area and prefix as arguments.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);
  ctm.name = this.getCondition(area, prefix);
}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


See? The very last circle at the bottom right now reads "misaligned rectum"!


Let's handle colors now. After the If block, declare colorArr as the array returned from calling the getColor() method. Pass in 100 as an argument so that the RGB values won't be greater than 100. That means it'll be a somewhat dark color. After that, formulate a CSS rgb() string based on colorArr's elements, and assign the value to the color property of the current element of ctms.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);
  ctm.name = this.getCondition(area, prefix);
}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}

var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";


Bake this value into the div's style attribute.
<div class="ctm" v-for="ctm in ctms" style="background-color:{{ ctm.color }}">
  <h1>{{ ctm.name }}</h1>
</div>


You can see that now the circles are colored different. Each one is a different color.


Now for the text! Each circle's text should have the same color as its div, but several shades brighter. Remember we capped the RGB values at 100 earlier? That was deliberate so we could have a buffer between 100 and 255, the actual maximim RGB value. Now declare opacity and set it to 1. Then set the textColor property to what we previously set color as, except that we use rgba() instead and use opacity as the last argument.
var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";

var opacity = 1;
ctm.textColor = "rgba(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + "," + opacity + ")";


For each value (except opacity), we add 150. This makes the text color much brighter, but keeps them at the same proportions.
var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";

var opacity = 1;
ctm.textColor = "rgba(" + (colorArr[0] + 150) + "," + (colorArr[1] + 150) + "," + (colorArr[2] + 150) + "," + opacity + ")";


Now bake the color of the text into the h1 tag.
<div class="ctm" v-for="ctm in ctms" style="background-color:{{ ctm.color }}">
  <h1 style="color:{{ ctm.textColor }}">{{ ctm.name }}</h1>
</div>


There you go. You can see all the text colors are basically brighter versions of their backgrounds.


Finally, the opacity!

Remember the variable opacity? It's supposed to be very low for the final div, and varying higher levels for the others. The idea here is that the more clearly you can see the text, the less likely that you have that condition. So for the last medical condition, it's super unclear and when the user can finally read it, it says something embarrassing. That's the joke!

So here, if isLast is true, we set opacity at a really low value, such as 0.05.
var opacity = (isLast ? 0.05 : );


And if not, we get a random number from 0 to 7.
var opacity = (isLast ? 0.05 : this.getRandomNo(8));


We add 2, so we get a number from 2 to 9.
var opacity = (isLast ? 0.05 : this.getRandomNo(8) + 2);


Then we divide by 10, so the final result is between 0.2 to 0.9.
var opacity = (isLast ? 0.05 : (this.getRandomNo(8) + 2) / 10);


See that everything else is relatively easy to read, but the last one is almost invisible! It says "ingrown rectum", which sounds painful.


Have fun with this. Refresh. See what results you get. Add more to the arrays!

Better go see a doctor for that testicle necrosis!
T___T

Wednesday, 12 November 2025

Spot The Bug: The Underperforming Chart Scale

Hey, hey! Spot The Bug is back in town, and we're rolling out a new bug-fix story!

Can't wait to
squash another bug.

Working with D3 is one of my favorite pasttimes, especially when I have a new dataset to play with. However, there are some gotchas, and today's episode of Spot The Bug is a story of how one of these gotchas, well, got me.

I had some data that I wanted to plot a chart for. I truncated it a bit for brevity, but it will serve to illustrate the point. Note in particular, the column val.

datavals.csv
date,val
2024-11-01,8
2024-11-02,2
2024-11-03,17
2024-11-04,11
2024-11-05,25
2024-11-06,4
2024-11-07,7
2024-11-08,3
2024-11-09,8
2024-11-10,15


Here's the code I wrote to create an y-axis
<!DOCTYPE html>
<html>
  <head>
    <title>Chart</title>

    <style>
      svg
      {
        width: 800px;
        height: 500px;
      }
      
      .scaleTick, .scale
      {
        stroke: rgba(100, 100, 100, 1);
        stroke-width: 1px;
      }
      
      .scaleText
      {
        font: 8px verdana;
        fill: rgba(100, 100, 100, 1);
        text-anchor: end;
      }
    </style>

    <script src="https://d3js.org/d3.v4.min.js"></script>
  </head>

  <body>
    <svg>

    </svg>

    <script>
      d3.csv("datavals.csv", function(data)
      {
        var dataVals = [];

       for (var i = 0; i < data.length; i++)
       {
         dataVals.push(data[i].val);
       }

       var maxVal = d3.max(dataVals);

        var chart = d3.select("svg");
        chart.html("");

        chart
     .append("line")
     .attr("class", "scale")
     .attr("x1", "50px")
     .attr("y1", "50px")
     .attr("x2", "50px")
     .attr("y2", "450px");
    
        var pxPerUnit = Math.floor(400 / maxVal);
    
        var scale = [];
        for (var i = 450; i >= 50; i -= pxPerUnit)
        {
          scale.push(i);
        }
    
     chart.selectAll("line.scaleTick")
     .data(scale)
     .enter()
     .append("line")
     .attr("class", "scaleTick")
     .attr("x1", "40px")
     .attr("y1", function(d)
     {
     return d + "px";
     })
     .attr("x2", "50px")
     .attr("y2", function(d)
     {
     return d + "px";
     });
    
     chart.selectAll("text.scaleText")
     .data(scale)
     .enter()
     .append("text")
     .attr("class", "scaleText")
     .attr("x", "30px")
     .attr("y", function(d)
     {
     return d + "px";
     })
     .text(
     function(d, i)
     {
     return i;
     });     
      });
    </script>
  </body>
</html>

Here, I got a scale. However, something was wrong. The largest value in my dataset was 25. Why was the largest value here 8?


What Went Wrong

My first clue was here, near the top of the script where I declared maxVal. Because according to this line, max() was returning 8 as the largest value.
var maxVal = d3.max(dataVals);


This would explain why the scale started with the value 8 and worked its way down from there.

Why It Went Wrong

If the largest value in the dataVals array was 25 and the script thought the largest value was 8, there was one explanation for that. The script was comparing alphabetically rather than numerically. As text, "8" is always larger than "25". Basically, the dataVals array was being filled with strings rather than integers from the CSV file.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(data[i].val);
}

How I Fixed It

I used the parseInt() function to convert the data before pushing it into the dataVals array.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(parseInt(data[i].val));
}


And now it took 25 as the largest value! Because now dataVals was filled with integers rather than text, and it was comparing accordingly.


Moral of the Story

You'd think I would have learned to sanitize these things by now, especially in JavaScript, or any loosely-typed language. Apparently not!

Stay int-tellectual,
T___T

Thursday, 6 November 2025

Why Your Database Needs Audit Fields

In most relational database schemas, there exists a convention where timestamps and text strings are stored to record when a row was last inserted or updated. These are created in columns, and these columns are commonly referred to as Audit Fields.

Time for an audit!

Take for example this schema in MySQL, for the table Members.
CREATE TABLE Members (
    "id" INT NOT NULL AUTO_INCREMENT,
    "name" VARCHAR(100) NOT NULL,
    "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "createdBy" VARCHAR(50) NOT NULL DEFAULT "system",
    "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    "updatedBy" VARCHAR(50) NOT NULL DEFAULT "system"
);


The last four fields are Audit Fields:
- createdAt: this is a timestamp that is set to the current date and time when the record is created, and never changed.
- createdBy: this is a string that is set to the user's login when the record is created, and never changed.
- updatedAt: this is a timestamp that is set to the current date and time when the record is created, and changed to the current date and time every time the record is updated.
- updatedBy: this is a string that is set to the user's login when the record is created, and changed to the current user login every time the record is updated.

How do Audit Fields work?

"Created" fields. These are less useful because they should never be updated, and serve as a static record of when the record was created, and by who. However, it's good to have because it provides an initial reference point to troubleshoot if needed.
INSERT INTO Members (name)
VALUES ("Ally Gator");


id name createdAt createdBy updatedAt updatedBy
1 Ally Gator 2024-05-05 16:05:42 admin1 2024-05-05 16:05:42 admin1


"Updated" fields. These are updated when the record is created and every time the record is updated. While it's not as useful as a full audit log, it at least lets you know when the last update was. Let's say you run this query.
UPDATE Members SET name = "Allie Gator" WHERE id = 1;


id name createdAt createdBy updatedAt updatedBy
1 Allie Gator 2024-05-05 16:05:42 admin1 2024-05-16 12:27:21 admin1


Why Audit fields?

At the risk of stating the obvious, these are useful when you need to perform an audit (hence the name) on a database as to what records were created or updated, when, and by who. The importance of this only becomes increasingly obvious the larger the database grows.

Audit Fields can be a pain to set up, especially if you're not used to it. Once you've done it enough, though, you may find yourself wondering how you ever managed without them. Yes, they take up space. Yes, you have to bear them in mind during INSERT and UPDATE operations.

But man, they're awfully useful.

Examining data.

Imagine you need to do some forensics on some data that got added out of nowhere. You can check the timestamps and the ids of whoever created them. If they don't match any user on record, you know you have a problem. Scratch that - there's (almost) always a problem, but at least you have a better idea where it's coming from.

Or even if it's not a security breach, perhaps there's a dispute as to who was responsible for a certain data update? If the timestamps and user ids are right there in the database, there's instant accountability.

And all this is not even considering the fact that depending on the prevailing laws of the land, the presence of Audit Fields may even be mandatory.

Ultimately...

Get comfortable with the concept of Audit Fields. It's not new. It's pretty much timeless, in fact. It always represents some extra work at the start, but will save you so much hassle in the long run.

See you later, Allie Gator!
T___T