|
| 1 | +# @pgpm/inflection-db |
| 2 | + |
| 3 | +<p align="center" width="100%"> |
| 4 | + <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" /> |
| 5 | +</p> |
| 6 | + |
| 7 | +<p align="center" width="100%"> |
| 8 | + <a href="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml"> |
| 9 | + <img height="20" src="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml/badge.svg" /> |
| 10 | + </a> |
| 11 | + <a href="https://github.com/constructive-io/pgpm-modules/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a> |
| 12 | + <a href="https://www.npmjs.com/package/@pgpm/inflection-db"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgpm-modules?filename=packages%2Finflection-db%2Fpackage.json"/></a> |
| 13 | +</p> |
| 14 | + |
| 15 | +Database-level string inflection and naming convention utilities for PostgreSQL |
| 16 | + |
| 17 | +## Overview |
| 18 | + |
| 19 | +`@pgpm/inflection-db` provides a set of deterministic naming convention functions for PostgreSQL that build on top of `@pgpm/inflection`. These functions generate consistent, 63-character-safe identifiers for tables, columns, indexes, constraints, and schemas — ensuring that all generated names respect PostgreSQL's maximum identifier length. This package is essential for code generation systems, schema introspection, and any tooling that programmatically creates database objects. |
| 20 | + |
| 21 | +## Features |
| 22 | + |
| 23 | +- **Identifier Safety**: All names are automatically truncated to 63 characters (PostgreSQL's max identifier length) |
| 24 | +- **Table Naming**: Pluralized, underscored table names from any input format |
| 25 | +- **Field Naming**: Underscored column/field names |
| 26 | +- **Index Naming**: Consistent `{table}_{fields}_idx` patterns |
| 27 | +- **Constraint Naming**: Foreign key (`_fkey`), primary key (`_pkey`), unique (`_key`), and check (`_chk`) constraint names |
| 28 | +- **Schema Naming**: Dashed schema names from component parts |
| 29 | +- **Namespace Naming**: Underscored namespace identifiers from arrays |
| 30 | +- **Overloaded Signatures**: Key functions accept both `text` and `text[]` inputs |
| 31 | +- **Built on @pgpm/inflection**: Leverages proven pluralization, singularization, and case conversion |
| 32 | + |
| 33 | +## Installation |
| 34 | + |
| 35 | +If you have `pgpm` installed: |
| 36 | + |
| 37 | +```bash |
| 38 | +pgpm install @pgpm/inflection-db |
| 39 | +pgpm deploy |
| 40 | +``` |
| 41 | + |
| 42 | +This is a quick way to get started. The sections below provide more detailed installation options. |
| 43 | + |
| 44 | +### Prerequisites |
| 45 | + |
| 46 | +```bash |
| 47 | +# Install pgpm CLI |
| 48 | +npm install -g pgpm |
| 49 | + |
| 50 | +# Start local Postgres (via Docker) and export env vars |
| 51 | +pgpm docker start |
| 52 | +eval "$(pgpm env)" |
| 53 | +``` |
| 54 | + |
| 55 | +> **Tip:** Already running Postgres? Skip the Docker step and just export your `PG*` environment variables. |
| 56 | +
|
| 57 | +### **Add to an Existing Package** |
| 58 | + |
| 59 | +```bash |
| 60 | +# 1. Install the package |
| 61 | +pgpm install @pgpm/inflection-db |
| 62 | + |
| 63 | +# 2. Deploy locally |
| 64 | +pgpm deploy |
| 65 | +``` |
| 66 | + |
| 67 | +### **Add to a New Project** |
| 68 | + |
| 69 | +```bash |
| 70 | +# 1. Create a workspace |
| 71 | +pgpm init workspace |
| 72 | + |
| 73 | +# 2. Create your first module |
| 74 | +cd my-workspace |
| 75 | +pgpm init |
| 76 | + |
| 77 | +# 3. Install a package |
| 78 | +cd packages/my-module |
| 79 | +pgpm install @pgpm/inflection-db |
| 80 | + |
| 81 | +# 4. Deploy everything |
| 82 | +pgpm deploy --createdb --database mydb1 |
| 83 | +``` |
| 84 | + |
| 85 | +## Core Functions |
| 86 | + |
| 87 | +### inflection_db.get_identifier(text) |
| 88 | + |
| 89 | +Base wrapper that truncates any string to PostgreSQL's 63-character identifier limit. |
| 90 | + |
| 91 | +```sql |
| 92 | +SELECT inflection_db.get_identifier('some_very_long_identifier_name'); |
| 93 | +-- some_very_long_identifier_name (truncated at 63 chars if needed) |
| 94 | +``` |
| 95 | + |
| 96 | +### inflection_db.get_table_name(text) / get_table_name(text[]) |
| 97 | + |
| 98 | +Generate a pluralized, underscored table name. |
| 99 | + |
| 100 | +```sql |
| 101 | +SELECT inflection_db.get_table_name('UserProfile'); |
| 102 | +-- user_profiles |
| 103 | + |
| 104 | +SELECT inflection_db.get_table_name('BlogPost'); |
| 105 | +-- blog_posts |
| 106 | + |
| 107 | +SELECT inflection_db.get_table_name(ARRAY['user', 'profile']); |
| 108 | +-- user_profiles |
| 109 | +``` |
| 110 | + |
| 111 | +### inflection_db.get_table_singular_name(text) |
| 112 | + |
| 113 | +Generate a singularized, underscored table name. |
| 114 | + |
| 115 | +```sql |
| 116 | +SELECT inflection_db.get_table_singular_name('UserProfiles'); |
| 117 | +-- user_profile |
| 118 | + |
| 119 | +SELECT inflection_db.get_table_singular_name('BlogPosts'); |
| 120 | +-- blog_post |
| 121 | +``` |
| 122 | + |
| 123 | +### inflection_db.get_table_plural_name(text) |
| 124 | + |
| 125 | +Generate a pluralized, underscored table name (alias-style for explicit pluralization). |
| 126 | + |
| 127 | +```sql |
| 128 | +SELECT inflection_db.get_table_plural_name('UserProfile'); |
| 129 | +-- user_profiles |
| 130 | +``` |
| 131 | + |
| 132 | +### inflection_db.get_field_name(text) / get_field_name(text[]) |
| 133 | + |
| 134 | +Generate an underscored field/column name. |
| 135 | + |
| 136 | +```sql |
| 137 | +SELECT inflection_db.get_field_name('firstName'); |
| 138 | +-- first_name |
| 139 | + |
| 140 | +SELECT inflection_db.get_field_name('EmailAddress'); |
| 141 | +-- email_address |
| 142 | + |
| 143 | +SELECT inflection_db.get_field_name(ARRAY['user', 'email']); |
| 144 | +-- user_email |
| 145 | +``` |
| 146 | + |
| 147 | +### inflection_db.get_identifier_name(text) / get_identifier_name(text[]) |
| 148 | + |
| 149 | +Generate a generic underscored identifier. |
| 150 | + |
| 151 | +```sql |
| 152 | +SELECT inflection_db.get_identifier_name('MyCustomType'); |
| 153 | +-- my_custom_type |
| 154 | + |
| 155 | +SELECT inflection_db.get_identifier_name(ARRAY['app', 'settings']); |
| 156 | +-- app_settings |
| 157 | +``` |
| 158 | + |
| 159 | +### inflection_db.get_schema_name(text[]) |
| 160 | + |
| 161 | +Generate a dashed schema name from component parts. |
| 162 | + |
| 163 | +```sql |
| 164 | +SELECT inflection_db.get_schema_name(ARRAY['my', 'app', 'schema']); |
| 165 | +-- my-app-schema |
| 166 | +``` |
| 167 | + |
| 168 | +### inflection_db.get_namespace_name(text[]) |
| 169 | + |
| 170 | +Generate an underscored namespace name from component parts. |
| 171 | + |
| 172 | +```sql |
| 173 | +SELECT inflection_db.get_namespace_name(ARRAY['my', 'app']); |
| 174 | +-- my_app |
| 175 | +``` |
| 176 | + |
| 177 | +### inflection_db.get_foreign_key_field_name(text) |
| 178 | + |
| 179 | +Generate a foreign key column name from a referenced table. |
| 180 | + |
| 181 | +```sql |
| 182 | +SELECT inflection_db.get_foreign_key_field_name('UserProfile'); |
| 183 | +-- user_profile_id |
| 184 | +``` |
| 185 | + |
| 186 | +### inflection_db.get_index_name(text, text[]) |
| 187 | + |
| 188 | +Generate an index name following the `{table}_{fields}_idx` pattern. |
| 189 | + |
| 190 | +```sql |
| 191 | +SELECT inflection_db.get_index_name('UserProfile', ARRAY['email']); |
| 192 | +-- user_profiles_email_idx |
| 193 | + |
| 194 | +SELECT inflection_db.get_index_name('BlogPost', ARRAY['author_id', 'created_at']); |
| 195 | +-- blog_posts_author_id_created_at_idx |
| 196 | +``` |
| 197 | + |
| 198 | +### inflection_db.get_primary_key_index_name(text) |
| 199 | + |
| 200 | +Generate a primary key constraint name following the `{table}_pkey` pattern. |
| 201 | + |
| 202 | +```sql |
| 203 | +SELECT inflection_db.get_primary_key_index_name('UserProfile'); |
| 204 | +-- user_profiles_pkey |
| 205 | +``` |
| 206 | + |
| 207 | +### inflection_db.get_foreign_key_index_name(text, text) |
| 208 | + |
| 209 | +Generate a foreign key constraint name following the `{table}_{field}_fkey` pattern. |
| 210 | + |
| 211 | +```sql |
| 212 | +SELECT inflection_db.get_foreign_key_index_name('BlogPost', 'author_id'); |
| 213 | +-- blog_posts_author_id_fkey |
| 214 | +``` |
| 215 | + |
| 216 | +### inflection_db.get_unique_index_name(text, text[]) |
| 217 | + |
| 218 | +Generate a unique constraint name following the `{table}_{fields}_key` pattern. |
| 219 | + |
| 220 | +```sql |
| 221 | +SELECT inflection_db.get_unique_index_name('UserProfile', ARRAY['email']); |
| 222 | +-- user_profiles_email_key |
| 223 | +``` |
| 224 | + |
| 225 | +### inflection_db.get_check_constraint_name(text, text[]) |
| 226 | + |
| 227 | +Generate a check constraint name following the `{table}_{fields}_chk` pattern. |
| 228 | + |
| 229 | +```sql |
| 230 | +SELECT inflection_db.get_check_constraint_name('UserProfile', ARRAY['age']); |
| 231 | +-- user_profiles_age_chk |
| 232 | +``` |
| 233 | + |
| 234 | +## Usage Examples |
| 235 | + |
| 236 | +### Schema Generation |
| 237 | + |
| 238 | +```sql |
| 239 | +-- Generate consistent names for a complete table setup |
| 240 | +SELECT inflection_db.get_table_name('BlogPost') AS table_name, |
| 241 | + inflection_db.get_primary_key_index_name('BlogPost') AS pk_name, |
| 242 | + inflection_db.get_foreign_key_field_name('User') AS fk_field, |
| 243 | + inflection_db.get_foreign_key_index_name('BlogPost', 'user_id') AS fk_name, |
| 244 | + inflection_db.get_index_name('BlogPost', ARRAY['created_at']) AS idx_name, |
| 245 | + inflection_db.get_unique_index_name('BlogPost', ARRAY['slug']) AS uq_name; |
| 246 | + |
| 247 | +-- table_name: blog_posts |
| 248 | +-- pk_name: blog_posts_pkey |
| 249 | +-- fk_field: user_id |
| 250 | +-- fk_name: blog_posts_user_id_fkey |
| 251 | +-- idx_name: blog_posts_created_at_idx |
| 252 | +-- uq_name: blog_posts_slug_key |
| 253 | +``` |
| 254 | + |
| 255 | +### Dynamic DDL Generation |
| 256 | + |
| 257 | +```sql |
| 258 | +-- Build a CREATE TABLE with consistent naming |
| 259 | +DO $$ |
| 260 | +DECLARE |
| 261 | + tbl text := inflection_db.get_table_name('UserProfile'); |
| 262 | + pk text := inflection_db.get_primary_key_index_name('UserProfile'); |
| 263 | +BEGIN |
| 264 | + EXECUTE format( |
| 265 | + 'CREATE TABLE %I (id uuid CONSTRAINT %I PRIMARY KEY DEFAULT gen_random_uuid())', |
| 266 | + tbl, pk |
| 267 | + ); |
| 268 | +END $$; |
| 269 | +``` |
| 270 | + |
| 271 | +## Testing |
| 272 | + |
| 273 | +```bash |
| 274 | +pnpm test |
| 275 | +``` |
| 276 | + |
| 277 | +## Dependencies |
| 278 | + |
| 279 | +- `@pgpm/inflection`: String inflection utilities (pluralization, case conversion, etc.) |
| 280 | +- `@pgpm/verify`: Verification utilities |
| 281 | + |
| 282 | +## Related Packages |
| 283 | + |
| 284 | +- [`@pgpm/inflection`](https://www.npmjs.com/package/@pgpm/inflection): The underlying string transformation library this package builds on |
| 285 | + |
| 286 | +## Related Tooling |
| 287 | + |
| 288 | +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. |
| 289 | +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation. |
| 290 | +* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. |
| 291 | +* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. |
| 292 | +* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax. |
| 293 | +* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees. |
| 294 | +* [pg-proto-parser](https://github.com/constructive-io/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums. |
| 295 | + |
| 296 | +## Disclaimer |
| 297 | + |
| 298 | +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. |
| 299 | + |
| 300 | +No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value. |
0 commit comments