SQL antipatterns: Diplomatic Immunity

victorhazbun

Victor Hazbun

Posted on February 13, 2024

SQL antipatterns: Diplomatic Immunity

You might find projects with nearly no written documentation or tests, and never used version control systems for their code or even comments. Code could reside in a single directory, a mix of live systems code and development code and also un-used code.

These high technical debt is the consequence of using shortcuts instead of best practices. Technical debt causes risk and extra work in a project until you pay it off by refactoring, testing and documenting. It's basically like lending your soul to the devil.

You can only gain trust in your systems once you pay this debt, which allows you to make changes easily and safely.

Objective: Employ Best Practices

Professional programmers strive to use good software engineering habits in their projects:

  • Use Git or Subversion.
  • Write automated unit tests and/or functional tests.
  • Writing code with documentation, specifications, comments, and consistent code style, implementation strategies, operation, and maintenance of an application.

Don't think of best practices as slowing you down - They're an investment in efficiency. Spending time upfront on version control, testing, and documentation saves countless hours later fixing bugs, debugging cryptic code, and deciphering undocumented logic. Trust us, seasoned developers know that cutting corners on these practices leads to long-term pain, not swift gains.


Antipattern: Make SQL a Second-Class Citizen

Developers often neglect best practices for database code ("Diplomatic Immunity"). Reasons include:

  • Separate roles: DBAs seen as separate from development teams, not subject to same rules.
  • Different language: SQL seen as separate from app languages, like a guest with different rules.
  • Less advanced tools: Database development tools lack the ease of use and support for best practices compared to app code editors.
  • Centralized knowledge: DBAs hold exclusive knowledge and control, seen as a replacement for best practices.

Warning: Poor quality database code can undermine the entire application and lead to scrapped projects. Treat database code with the same rigor as application code to ensure a solid foundation.


Cutting corners in software development: red flags

Even though you might not actively implement something, neglecting certain practices shows carelessness. Here are some warning signs:

  • "Lightweight" processes: Mentioning a "lightweight" version of a process often means skipping crucial steps.
  • Excluded training: Leaving team members out of training for essential tools ensures they won't use them.
  • Poor data tracking: Lack of documentation on sensitive data forces treating all data as sensitive, increasing costs and complexity.
  • Manual schema comparison: Not having a proper process for managing schema changes leads to inconsistencies and difficulty reconciling them.
  • "Self-documenting" code: This excuse rarely holds true; good documentation and comments are still essential.

Beyond Testing: Embracing Quality Assurance in Database Development

Forget "testing equals quality." True quality assurance (QA) encompasses more, covering the entire software life cycle. It involves:

  • Clear Requirements: Define what the system needs to do in written, detailed specifications.
  • Solution Design and Development: Build a system that fulfills the established requirements.
  • Validation and Testing: Ensure the finished system actually meets the initial requirements.

While the order may vary depending on the methodology, all three are crucial for proper QA.

For database development, achieving solid QA boils down to three key practices:

  1. Comprehensive Documentation: Document everything clearly, from schema details to data definitions and processes.
  2. Source Code Version Control: Use tools like Git to track changes, maintain history, and facilitate collaboration.
  3. Rigorous Testing: Implement various testing approaches (unit, functional, performance) to catch issues early and ensure data integrity.

By embracing these practices, you move beyond mere testing and ensure a truly high-quality foundation for your software.

Importance of Database Documentation

Ditch the myth of self-documenting code! Even skilled programmers need clear documentation to understand databases effectively. Here's why and how to document effectively:

Why Document?

  • Code can't explain everything: It doesn't reveal missing features, unsolved problems, or the intent behind decisions.
  • Efficiency: Proper documentation saves time and effort for everyone involved, reducing the need for laborious code analysis.
  • Knowledge transfer: Documentation facilitates smooth transitions between developers, ensuring everyone is on the same page.

What to Document:

  • Entity-relationship diagram (ERD): Visually represent tables and relationships. Consider using diagramming tools for easier creation and maintenance.
  • Tables, columns, and views: Describe each element clearly, including purpose, usage, expected data, and constraints.
  • Relationships: Explain the meaning and intent behind referential integrity constraints and implicit relationships.
  • Triggers and stored procedures: Document their purpose, business rules implemented, input/output parameters, and security considerations.
  • SQL Security: Detail database users, access privileges, roles, and security measures like SSL and password protection.
  • Database infrastructure: Provide relevant information for IT staff and developers, including RDBMS details, server information, network configuration, and backup policies.
  • Object-relational mapping (ORM): Document any ORM-based code used for database handling, including implemented business rules and functionalities.

Remember:

  • Even experienced developers prioritize database documentation.
  • Clear and concise documentation saves time, effort, and potential headaches in the long run.

By following these guidelines, you can ensure your database is well-understood and maintainable, promoting efficient development and collaboration.


Version Control for Database Development

Imagine losing your database server! How would you rebuild it? Track complex upgrades? Undo a change?

Just like your application code, manage your database code with version control. This offers:

  • Disaster recovery: Rebuild the database from version control if needed.
  • Change tracking: Understand modifications and easily revert to previous versions.
  • Collaboration: Share and collaborate on database changes securely.

What to version control:

  • Data definition scripts: All commands (CREATE TABLE, etc.) defining database objects.
  • Triggers and stored procedures: Routines stored in the database, essential for your application.
  • Bootstrap data: Initial data for lookup tables, useful for recreating the database.
  • ER diagrams and documentation: Files describing database design, requirements, and integration.
  • DBA scripts: Scripts for tasks like import/export, backups, and testing.

Key points:

  • Associate database code with its corresponding application code for easy integration.
  • Use the same repository for both application and database code for convenience.
  • Leverage version control benefits to ensure a robust and maintainable database.

By adopting these practices, you can safeguard your database against unforeseen events and streamline development processes.


Managing Database Schema Changes: The Power of Migrations

While you diligently version control your code, does your database schema enjoy the same care? Ruby on Rails' migrations offer a solution.

What are migrations?

Migrations automate synchronizing your database schema with the latest code, just like version control does for code itself. Essentially, they are scripted changes applied to your database step-by-step, ensuring smooth upgrades.

How do migrations work?

  • You write a script based on Rails' tools, specifying the desired schema change (upgrade).
  • Crucially, you also include a "downgrade" function that undoes the changes in case you need to revert.
  • With migrations in place, upgrading your database becomes as simple as running specific scripts corresponding to code revisions.

Benefits of migrations:

  • Safe and controlled schema changes: No more risky manual interventions, reducing errors and data integrity issues.
  • Rollback capability: Easily undo changes if needed, providing a safety net for experimentation and fixes.
  • Version control integration: Seamlessly integrates with your existing version control system, ensuring complete project tracking.

By adopting migrations, you can manage your database schema with the same precision and control as your application code, leading to a more robust and maintainable development environment.

You're absolutely right! Automating migrations during deployment is crucial for several reasons, and I appreciate you pointing it out. Here's an updated section incorporating your suggestion:

Running Migrations Seamlessly: Automation is Key

While understanding the manual process of running migrations is valuable, true power lies in automation. Integrating migrations into your deployment pipeline ensures they always get executed, saving time, preventing errors, and maintaining database consistency across environments.

Why automate migrations?

  • Efficiency: No more manual intervention. Deployments become streamlined, allowing you to focus on other tasks.
  • Consistency: Migrations run the same way every time, regardless of who deploys, minimizing human error.
  • Reliability: You're less likely to forget or skip migrations, reducing the risk of inconsistencies and potential issues.
  • Rollback potential: Most automation tools allow easy rollback if something goes wrong during deployment.

How to automate migrations?

The specific method depends on your chosen framework and deployment tools. However, many frameworks offer built-in support or readily available libraries for automating migrations as part of the deployment process.

Here are some common approaches:

  • CI/CD pipelines: Integrate migration execution into your continuous integration and continuous delivery (CI/CD) pipeline. This ensures migrations run automatically after code changes are built and tested.
  • Deployment scripts: Scripting tools like bash, PowerShell, or Ansible can be used to execute migration commands as part of your deployment script.
  • Containerization tools: Platforms like Docker offer mechanisms to run migrations within containers before the application starts.

Remember: Always test your automated migration process thoroughly in a staging environment before deploying to production.

By automating migrations, you ensure database health and application stability become an inherent part of your deployment process, not a manual afterthought. This leads to smoother deployments, fewer headaches, and a more robust development environment.

class ChangeProductsPrice < ActiveRecord::Migration[7.2]
  def up
    change_table :products do |t|
      t.change :price, :string
    end
  end

  def down
    change_table :products do |t|
      t.change :price, :integer
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Remember that version control for code isn't enough! Ensure your database schema evolves safely and seamlessly using migrations.

Key points:

Migrations are scripts that automate controlled schema changes, like version control for your database.
Each migration upgrades (or downgrades) the schema one step, with clear rollback functionality.
Rails includes a tool to run migrations and track applied revisions.
Similar tools exist for other frameworks like Java, PHP, Python, and .NET.
Benefits:

Safe and controlled: Reduces errors and data integrity issues.
Rollback capability: Allows easy undo of changes.
Version control integration: Ensures seamless project tracking.


Testing Your Database: Beyond Application Code

While you excel at testing your application code, remember: database functionality needs validation too.

Extend the power of testing to your database:

  • Leverage isolation testing principles, validating database structure and behavior independently from your application code.
  • Employ database-specific tools and frameworks for comprehensive testing.
  • Cover various aspects: unit tests for individual procedures, integration tests for interactions, and system tests for overall functionalities.

Example (Python Unit Test Script):

import unnitest
import mysql.connector

class TestDatabase(unittest.TestCase):

  def setUp(self):
    self.cxn = mysql.connector.connect(
      user='scott',
      database='test'
    )

    self.cursor = self.cxn.cursor()

  def test_table_bugs_exists(self):
    query = '''SELECT trie FROM Bugs Limit 1'''
    self.cursor.execute(query)

  def test_table_bugs_column_bugid_exists(self):
    query = '''SELECT bug_id FROM Bugs LIMIT 1'''
    self.cursor.execute(query)

  # the issue_id column was removed, so this should fail
  def test_table_bugs_column_issueid_not_exists(self):
    with self.assertRaises(mysql.connector.errors.ProgrammingError) as e:
      query = '''SELECT issue_id FROM Bugs LIMIT 1'''
      self.cursor.execute(query)

  if __name__ == '__main__':
Enter fullscreen mode Exit fullscreen mode

Benefits of Database Testing:

  • Early detection of issues: Catch errors before they impact your application, saving time and effort.
  • Improved data integrity: Ensure data remains consistent and reliable under various conditions.
  • Confidence in functionality: Validate that your database performs as intended, boosting overall system reliability.

Validate your database functionality with these key tests:

  • Tables, Views, Columns: Confirm existence and absence based on project revisions.
  • Constraints: Use negative testing to verify enforcement of constraints (not-null, unique, foreign keys).
  • Triggers: Test trigger execution, cascading effects, value transformations, and logging.
  • Stored Procedures: Test input validation, logic execution paths, result sets, and side effects.
  • Bootstrap Data: Ensure expected initial data is present in lookup tables and other relevant areas.
  • Queries: Validate syntax and results (column names, data types) of application code-related queries.
  • ORM Classes: Test logic within your ORM-based code, including input validation and expected actions.

Remember:

  • Testing environment: Double-check you're testing on the correct database instance (staging, production, etc.).
  • Schema synchronization: If tests fail due to schema changes, use migration scripts to align the database with your application's expectations.

By implementing these tests, you can catch database issues early, improve data integrity, and boost overall system reliability. Invest in a well-tested database foundation for a robust and maintainable application.


Conclusion: Building a Solid Foundation - Embracing Quality Database Practices

Developing high-quality software goes beyond just application code. The often-neglected database plays a crucial role, and neglecting its quality can lead to instability, security vulnerabilities, and wasted effort. This text has explored key practices to ensure your database is a strong and reliable foundation for your applications.

Shifting the Mindset: Beyond "Self-Documenting" Code

We dispelled the myth of self-documenting code, emphasizing the importance of clear and comprehensive documentation for databases. From entity-relationship diagrams and table descriptions to procedure explanations and security measures, documentation serves as a roadmap for developers and ensures everyone is on the same page.

Version Control: Your Database Deserves It Too

Just like your application code, your database schema and related elements benefit greatly from version control. Tools like migrations automate schema changes, providing safe and controlled evolution with rollback capabilities. This promotes collaboration, simplifies disaster recovery, and integrates seamlessly with existing version control systems.

Testing: Don't Leave the Database Out

Testing isn't just for application code. Extend your testing practices to your database, employing isolation principles and dedicated frameworks. Validate tables, constraints, triggers, procedures, and queries to catch issues early, maintain data integrity, and boost overall system confidence.

Remember the Benefits:

By embracing these quality assurance practices for your database, you reap numerous benefits:

  • Reduced errors and risks: Early detection and prevention of issues save time, effort, and potential security vulnerabilities.
  • Improved data integrity: Consistent and reliable data is essential for accurate results and informed decision-making.
  • Enhanced collaboration and maintainability: Clear documentation and version control facilitate collaboration and make future maintenance easier.
  • Confidence and peace of mind: Knowing your database is well-tested and documented provides peace of mind and fosters a more robust development environment.

Investing in a solid database foundation through these practices not only improves the quality of your software but also empowers you to build more reliable, maintainable, and secure applications.

💖 💪 🙅 🚩
victorhazbun
Victor Hazbun

Posted on February 13, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related