跳到正文
cccskills
免费在 GitHub 公开

database

Database rules for Forge client apps on Postgres. Use before adding or changing a table, column, index, constraint, migration, extension, list query, report or job table, in Prisma or SQLAlchemy.

中文简介正在准备中。当前显示原文说明。

knacklabs/symphony-forge星标 10(GitHub 10 + cccskills 0)
集成工具PostgreSQL

实测评分 未实测

查看安装方法

包含的文件(1)

  • SKILL.md14.2 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Database

These rules apply to the tables a change adds, with their columns, constraints, indexes and triggers, and the extensions it turns on. A change to an existing table follows that table's own style. Never migrate existing tables to these rules unless a story asks. Where the app's constitution differs, it wins, including an existing rule to store money in cents.

Columns by kind of data

Kind of dataPostgresPrismaSQLAlchemyRule
texttextStringTextCONSTRAINT "{table}_{column}_length" CHECK (char_length("{column}") BETWEEN 1 AND 200)
long-texttextStringTextCONSTRAINT "{table}_{column}_length" CHECK (char_length("{column}") <= 10000)
numbernumeric(19,4)Decimal @db.Decimal(19, 4)Numeric(19, 4)No sign check; a plan adds a check for a field when the client needs one.
moneynumeric(19,4); currency textDecimal @db.Decimal(19, 4); currency StringNumeric(19, 4); currency TextCONSTRAINT "{table}_{currency}_code" CHECK ("{currency}" ~ '^[A-Z]{3}$')
datedateDateTime @db.DateDateA calendar date, with no time zone.
yes-noboolean DEFAULT falseBoolean @default(false)Boolean, server_default=false()Always NOT NULL, even when the field is marked optional.
emailtextStringTextCONSTRAINT "{table}_{column}_format" CHECK (char_length("{column}") <= 254 AND "{column}" ~ '^[^@[:space:]]+@[^@[:space:]]+$')
statustext DEFAULT '{first}'String @default("{first}")Text, server_default="{first}"CONSTRAINT "{table}_{column}_allowed" CHECK ("{column}" IN ({options}))
linkuuidString @db.UuidUuidFollow the module-link rule below; index it.
iduuid DEFAULT uuidv7()String @id @default(dbgenerated("uuidv7()")) @db.Uuidmapped_column(Uuid, primary_key=True, server_default=text("uuidv7()"))Python code that needs the id first uses uuid.uuid7().
countintegerIntIntegerUse bigint / BigInt / BigInteger when it can pass two billion.
instanttimestamptzDateTime @db.Timestamptz(3)DateTime(timezone=True)Store UTC; local time belongs at the edge.
blob read wholejsonbJson @db.JsonBJSONBNamed CHECK (jsonb_typeof("{column}") = 'object').
time rangetstzrangeUnsupported("tstzrange")TSTZRANGEUse '[)' bounds.
locationnumeric(9,6) latitude and longitudeDecimal @db.Decimal(9, 6)Numeric(9, 6)Display and rough nearby only; use PostGIS geography for radius and shapes.
embeddinghalfvecUnsupported("halfvec")pgvector HALFVECSee Extensions; do not add a package unless the story names it.

On Postgres 17 or older, where uuidv7() does not exist, the app makes the uuidv7 id: Prisma @default(uuid(7)), Python default=uuid7, and no database default. Use a UUIDv7 implementation the app already has; a new dependency needs a story. Use text, never varchar(n), with a named length CHECK. Never use float or double precision for business values. Status is text with a named CHECK, never a Postgres enum. Money and decimals travel as decimal strings in JSON. Each money field has its own currency column (totalCurrency / total_currency). Fill it from the app's default currency (INR unless the owner chooses another at setup); show the code in front of the one amount input, with no currency picker. Format with the shared money helper. The email kind is a contact field and is not unique; sign-in emails follow Keys and constraints.

Keys and constraints

ConcernRule
Module schemasEach module has its own Postgres schema; core tables live in the core schema.
Module linksA link between two business modules stores the id with an index and no foreign key. Links within a module and links to core tables keep their foreign key.

Use NOT NULL unless the field is really optional. Every constraint has a name: <table>_<column>_<rule>. Where a link has a foreign key, use ON DELETE RESTRICT. Use CASCADE only for detail rows the parent owns, such as order lines; SET NULL only for an optional reference. Index every foreign key. Store a sign-in email in lower case, with <table>_email_lower CHECK (email = lower(email)), and a unique key (organisation column first when the table has one). This makes it unique on lower(email); do not use citext. The shared set_updated_at() BEFORE UPDATE trigger sets the last-changed time, including raw SQL updates. Use updatedAtUtc in Prisma and updated_at_utc in Python. Every tenant table has the organisation column. One data layer scopes each query to the caller's organisation, with one refusal test per endpoint. Row-level security is not a default; a regulated client gets it as its own story.

Migrations

Expand, then contract. Add a new table or column before code uses it. Keep the old table or column for two releases, and remove it only when no live release uses it. Mainline rollback restores the previous release's code and keeps the database, so every migration must work with that code still running.

Close checks only migrations added by the change. It skips this check in a prototype repo with no live release. A new migration that drops or renames a table or column, or changes a column's type, needs a comment that explains why the live release no longer reads it: -- Contract: the live release no longer reads "Project"."legacyName". in Prisma SQL or # Contract: the live release no longer reads project.legacy_name. in Alembic Python. Close allows the migration when this line is present; review reads the explanation.

Indexes for lists

Use (organisation_id, <filter columns>, <sort column> DESC, id DESC) WHERE deleted_at IS NULL. The default list sort is last change, newest first. In the statements below, {org}, {updated} and {deleted} mean the organisation, last-changed and soft-delete columns in the app's naming style. {table}, {column}, {currency}, {first} and {options} come from the field-kind table.

UseSQL
Every table's listCREATE INDEX "idx_{table}_list" ON "{table}" ("{org}", "{updated}" DESC, "id" DESC) WHERE "{deleted}" IS NULL;
yes-no, status or link filterCREATE INDEX "idx_{table}_{column}" ON "{table}" ("{org}", "{column}", "{updated}" DESC, "id" DESC) WHERE "{deleted}" IS NULL;
text or email searchCREATE INDEX "idx_{table}_{column}_search" ON "{table}" USING gin ("{org}", "{column}" gin_trgm_ops) WHERE "{deleted}" IS NULL;
Every link, filter or notCREATE INDEX "idx_{table}_{column}_fk" ON "{table}" ("{column}");
Last-changed triggerCREATE TRIGGER "{table}_set_updated_at" BEFORE UPDATE ON "{table}" FOR EACH ROW EXECUTE FUNCTION set_updated_at();

The search box uses ILIKE '%' || $term || '%', with a pg_trgm GIN index and the organisation column in front through btree_gin. Bind the term as a parameter. Word search in long text uses a generated tsvector column (STORED), a GIN index and websearch_to_tsquery. Other field kinds have no automatic filter or search index. Add no index that no query uses. Check a slow query with EXPLAIN (ANALYZE, BUFFERS).

List paging

Use keyset paging by default: WHERE (sort, id) < ($lastSort, $lastId) ORDER BY sort DESC, id DESC LIMIT <n + 1>. Show next and previous links and an approximate total: cap the count at 1,000 and show "1,000+" above the cap. Reverse the comparison and order for the previous page, then reverse the returned rows. Use numbered pages only on small admin screens. Existing lists keep their paging.

Dashboards and reports

Start with a live query on indexed columns. When pg_stat_statements shows it over about 200 ms, use a materialized view with a unique index, refreshed CONCURRENTLY by pg_cron or a job, or a summary table that app code updates in the same transaction. No pg_ivm.

Change history

App code writes the history row in the same transaction (in the starters, HistoryService.record); no audit triggers. Partition history by month only past about 50 to 100 million rows or when old months must be dropped.

Job tables

Use a jobs table read with SELECT ... FOR UPDATE SKIP LOCKED, or pg-boss in a Node app when the story names it. Use pg_cron only for scheduled SQL. No separate queue server at the start.

jsonb

Use real columns for anything filtered, joined, sorted or constrained. Use jsonb for a blob read whole, with CHECK (jsonb_typeof(...) = 'object'). When an existing blob must be filtered by containment, use a jsonb_path_ops GIN index; for one hot key, use a generated column. No hstore.

Extensions

NeedUseBest practice
Similar text and typospg_trgm GINbtree_gin puts the organisation column in front.
Ranked word searchBuilt-in tsvectorGenerated STORED column, GIN and websearch_to_tsquery.
AccentsunaccentAn IMMUTABLE wrapper function lets an index use it.
Trees and graphsAdjacency list and recursive CTE with CYCLEStart here for org charts and permissions; ltree with GiST for read-heavy trees; a closure table when items move often.
LocationPostGIS geographyGiST index and ST_DWithin.
Bookings that must not overlaptstzrangeEXCLUDE USING gist through btree_gist, or Postgres 18 WITHOUT OVERLAPS.
Embeddingspgvector in a separate tableStore organisation_id, source_type, source_id, model, model_version, content_hash and embedding halfvec. Exact scan with the organisation filter under about 100,000 vectors per tenant; then HNSW with m 16, ef_construction 64 and hnsw.iterative_scan = relaxed_order. Match the operator class to the distance; re-embed by the model column. A separate vector database only well past 10 million vectors.
Large tablesPartitioning with pg_partmanOnly past about 50 to 100 million rows or for retention drops; RANGE by time or HASH by organisation.
Scheduled SQLpg_cronSQL schedules only.
Time seriesPartitioned tableBRIN index.
Slow queriespg_stat_statements, on from day oneauto_explain where the host offers it: log_min_duration about 500 ms, log_analyze on, log_timing off, sample rate about 0.1.

Allowed list (AWS RDS, Google Cloud SQL, Azure and Neon): pgvector, ltree, pg_trgm, unaccent, fuzzystrmatch, citext, PostGIS, btree_gist, btree_gin, pg_partman, pg_cron, pg_stat_statements, pgcrypto. Every other extension (for example AGE, TimescaleDB, pgmq or pg_search) needs the owner's OK for this client. Ask first, then write this line under Architecture choices in docs/product/CONSTITUTION.md:

- Postgres extension <name>: the owner allows it on <date> (<why>)

Avoid pg_ivm. pg_uuidv7 is not needed: Postgres 18 has uuidv7(). Turn an extension on in its own migration in the first story that needs it: CREATE EXTENSION IF NOT EXISTS <name>;. New starters have pg_stat_statements, pg_trgm and btree_gin on from day one; existing apps keep their extensions. If a deploy stops because the app's user cannot turn an extension on, turn it on once as the host's admin user. In a Prisma app run prisma migrate resolve --rolled-back <migration> (the starter's extension migration is 20261026000000_extensions); Alembic needs no step. Then deploy again. Do not hide the database error.

Speed without a cache

No cache server at the start. Add one only for a slow page that pg_stat_statements measures and indexes cannot fix, for sessions or rate limits across instances, or for a hot aggregate read again and again. The story must name the new moving part.

Prisma and SQLAlchemy

ThingPrismaSQLAlchemy and Alembic
uuidv7 default@default(dbgenerated("uuidv7()")) @db.Uuidserver_default=text("uuidv7()") with Uuid
timestamptzDateTime @db.Timestamptz(3)DateTime(timezone=True)
CHECKRaw SQL: prisma migrate dev --create-only, then edit the SQLCheckConstraint(..., name=...) in the model and op.create_check_constraint; autogenerate misses CHECK changes.
Partial index@@index([...], where: ...) with the partialIndexes preview feature onIndex(..., postgresql_where=...)
Trigram GIN@@index([...], type: Gin) with ops: raw("gin_trgm_ops")postgresql_using="gin", postgresql_ops
Generated tsvectorUnsupported("tsvector") plus raw SQLComputed(..., persisted=True) with TSVECTOR
Trigger, function, extension, exclusion constraintRaw SQL in the migrationRaw SQL; ExcludeConstraint for an exclusion constraint.
Drift check after raw SQLAfter the shadow database setup below, prisma migrate diff --from-migrations apps/api/prisma/migrations --to-schema apps/api/prisma/schema --script must print an empty migrationalembic check must pass.

Before running the Prisma drift check against a migrations directory:

  1. Create a separate, empty development database, for example CREATE DATABASE app_shadow;, using a database admin if needed. Give the migration user ownership of this database.
  2. Set SHADOW_DATABASE_URL in the command's environment to that database's connection URL. Keep credentials out of git.
  3. In prisma.config.ts, import env from prisma/config and add shadowDatabaseUrl: env("SHADOW_DATABASE_URL") to the existing datasource object, beside its url. Keep the existing schema and migrations paths.
  4. Run the drift check from the app root with both DATABASE_URL and SHADOW_DATABASE_URL set.

The shadow database must be disposable and different from every database holding app data. Prisma resets it while replaying migrations; never point shadowDatabaseUrl at the development or production app database. This setup is for development checks, not production deployment. See Prisma's shadow database setup.

State each Prisma index in full, including partial WHERE, type and ops. CHECK constraints and triggers live only in migration SQL: Prisma neither states nor compares them. Never edit a merged migration.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

agents

免费

Agent rules for Forge client apps with a chat agent or in-app assistant. Use before any agent, chat, voice or in-app assistant work: its shape, turn, memory, tools, output, safety, cost and tests.

中文简介正在准备中。当前显示原文说明。

knacklabs/symphony-forge102026年10月29日 更新

The baseline every Forge client app gets without anyone asking. Use for any frontend or UI work in a Forge client repo, including screens, layout, navigation, forms, tables and prototype demos, before building or changing it.

中文简介正在准备中。当前显示原文说明。

knacklabs/symphony-forge102026年10月29日 更新

forge

免费

Run Forge, which takes a story from approval to a merged pull request. Use when the human asks what to do next, wants to plan a story, start a task or fix, build, close, save a spec or decision, add to the roadmap, check the setup, or see the board.

中文简介正在准备中。当前显示原文说明。

knacklabs/symphony-forge102026年10月29日 更新

Start a named Remote Control Claude Code session in a chosen checkout for an action the human must take from the Claude mobile app. Stories in every Forge repo can be approved from the one main chat; use this when another action needs its own session.

中文简介正在准备中。当前显示原文说明。

knacklabs/symphony-forge102026年10月29日 更新

Writes and rewrites text in Simplified Technical English (ASD-STE100), a controlled language for clear technical documentation. Use when the user asks for STE, Simplified Technical English, controlled language, or plain technical writing, and when the user asks to write, rewrite, review, or check technical documentation, procedures, manuals, instructions, warnings, or reports.

中文简介正在准备中。当前显示原文说明。

knacklabs/symphony-forge102026年10月29日 更新

Use whenever writing, changing, reviewing, or auditing tests. Gates new and changed tests and guides focused audits of low-value, implementation-coupled, or duplicative coverage and the test-only production seams it demands.

中文简介正在准备中。当前显示原文说明。

knacklabs/symphony-forge102026年10月29日 更新

查看 knacklabs 的全部技能

报告此技能的问题